This is an automated email from the ASF dual-hosted git repository.
voonhous pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hudi.git
The following commit(s) were added to refs/heads/master by this push:
new b90627ae393d test(common): add unit coverage for metrics reporters and
schema utilities (#19221)
b90627ae393d is described below
commit b90627ae393dd3d9b4b846ee85beb96c3aa8d5a8
Author: Y Ethan Guo <[email protected]>
AuthorDate: Tue Jul 21 01:16:28 2026 -0700
test(common): add unit coverage for metrics reporters and schema utilities
(#19221)
Adds unit tests for three under-covered hudi-common areas:
- TestValueType: per-type conversion, cast, and round-trip coverage across
numeric, string, bytes, decimal, UUID, date, time, and timestamp helpers,
plus fromParquetPrimitiveType, fromSchema, and fromOrdinal.
- TestHoodieSchemaTypePromotion: promotion matrix (widening, narrowing,
string<->bytes, unrelated types) and decimal widening rules.
- TestM3ScopeReporterAdaptor: registry-to-scope mapping for counters,
gauges, histograms, meters, and timers using a mocked Scope.
Co-authored-by: sivabalan <[email protected]>
---
.../schema/TestHoodieSchemaTypePromotion.java | 138 +++++++++++++
.../apache/hudi/metadata/stats/TestValueType.java | 229 +++++++++++++++++++++
.../metrics/m3/TestM3ScopeReporterAdaptor.java | 163 +++++++++++++++
3 files changed, 530 insertions(+)
diff --git
a/hudi-common/src/test/java/org/apache/hudi/common/schema/TestHoodieSchemaTypePromotion.java
b/hudi-common/src/test/java/org/apache/hudi/common/schema/TestHoodieSchemaTypePromotion.java
new file mode 100644
index 000000000000..e6215b9a19aa
--- /dev/null
+++
b/hudi-common/src/test/java/org/apache/hudi/common/schema/TestHoodieSchemaTypePromotion.java
@@ -0,0 +1,138 @@
+/*
+ * 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.common.schema;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Unit tests for the internal promotion matrix used by schema compatibility
checks.
+ */
+public class TestHoodieSchemaTypePromotion {
+
+ @Test
+ public void testSameTypeAlwaysPromotable() {
+ for (HoodieSchemaType type : HoodieSchemaType.values()) {
+ assertTrue(HoodieSchemaTypePromotion.canPromote(type, type),
+ "same type should promote to itself: " + type);
+ }
+ }
+
+ @Test
+ public void testIntWidening() {
+ assertTrue(HoodieSchemaTypePromotion.canPromote(HoodieSchemaType.LONG,
HoodieSchemaType.INT));
+ assertTrue(HoodieSchemaTypePromotion.canPromote(HoodieSchemaType.FLOAT,
HoodieSchemaType.INT));
+ assertTrue(HoodieSchemaTypePromotion.canPromote(HoodieSchemaType.DOUBLE,
HoodieSchemaType.INT));
+ }
+
+ @Test
+ public void testLongWidening() {
+ assertTrue(HoodieSchemaTypePromotion.canPromote(HoodieSchemaType.FLOAT,
HoodieSchemaType.LONG));
+ assertTrue(HoodieSchemaTypePromotion.canPromote(HoodieSchemaType.DOUBLE,
HoodieSchemaType.LONG));
+ // LONG cannot read a wider reader-narrows case
+ assertFalse(HoodieSchemaTypePromotion.canPromote(HoodieSchemaType.LONG,
HoodieSchemaType.FLOAT));
+ assertFalse(HoodieSchemaTypePromotion.canPromote(HoodieSchemaType.LONG,
HoodieSchemaType.DOUBLE));
+ }
+
+ @Test
+ public void testFloatWidening() {
+ assertTrue(HoodieSchemaTypePromotion.canPromote(HoodieSchemaType.DOUBLE,
HoodieSchemaType.FLOAT));
+ assertFalse(HoodieSchemaTypePromotion.canPromote(HoodieSchemaType.FLOAT,
HoodieSchemaType.DOUBLE));
+ }
+
+ @Test
+ public void testNarrowingNotAllowed() {
+ // reader cannot narrow: long data cannot be read by an int reader, etc.
+ assertFalse(HoodieSchemaTypePromotion.canPromote(HoodieSchemaType.INT,
HoodieSchemaType.LONG));
+ assertFalse(HoodieSchemaTypePromotion.canPromote(HoodieSchemaType.INT,
HoodieSchemaType.FLOAT));
+ assertFalse(HoodieSchemaTypePromotion.canPromote(HoodieSchemaType.INT,
HoodieSchemaType.DOUBLE));
+ assertFalse(HoodieSchemaTypePromotion.canPromote(HoodieSchemaType.LONG,
HoodieSchemaType.DOUBLE));
+ // FLOAT reader CAN read LONG writer (Avro promotion allows this despite
the mantissa
+ // precision loss). Asserted here as canonical to guard against
regressions.
+ assertTrue(HoodieSchemaTypePromotion.canPromote(HoodieSchemaType.FLOAT,
HoodieSchemaType.LONG));
+ }
+
+ @Test
+ public void testStringBytesBidirectional() {
+ assertTrue(HoodieSchemaTypePromotion.canPromote(HoodieSchemaType.STRING,
HoodieSchemaType.BYTES));
+ assertTrue(HoodieSchemaTypePromotion.canPromote(HoodieSchemaType.BYTES,
HoodieSchemaType.STRING));
+ // STRING can also read numeric writer types
+ assertTrue(HoodieSchemaTypePromotion.canPromote(HoodieSchemaType.STRING,
HoodieSchemaType.INT));
+ assertTrue(HoodieSchemaTypePromotion.canPromote(HoodieSchemaType.STRING,
HoodieSchemaType.DOUBLE));
+ // BYTES cannot read numeric types
+ assertFalse(HoodieSchemaTypePromotion.canPromote(HoodieSchemaType.BYTES,
HoodieSchemaType.INT));
+ }
+
+ @Test
+ public void testUnrelatedTypesNotPromotable() {
+ assertFalse(HoodieSchemaTypePromotion.canPromote(HoodieSchemaType.BOOLEAN,
HoodieSchemaType.INT));
+ assertFalse(HoodieSchemaTypePromotion.canPromote(HoodieSchemaType.INT,
HoodieSchemaType.BOOLEAN));
+ assertFalse(HoodieSchemaTypePromotion.canPromote(HoodieSchemaType.LONG,
HoodieSchemaType.STRING));
+ }
+
+ @Test
+ public void testDecimalWideningSameSizeIncreasedPrecision() {
+ HoodieSchema writer = fixedDecimal(8, 10, 2);
+ HoodieSchema reader = fixedDecimal(8, 15, 2);
+ assertTrue(HoodieSchemaTypePromotion.isDecimalWidening(reader, writer));
+ }
+
+ @Test
+ public void testDecimalWideningIdenticalIsAllowed() {
+ HoodieSchema writer = fixedDecimal(8, 10, 2);
+ HoodieSchema reader = fixedDecimal(8, 10, 2);
+ assertTrue(HoodieSchemaTypePromotion.isDecimalWidening(reader, writer));
+ }
+
+ @Test
+ public void testDecimalWideningRejectsDecreasedPrecision() {
+ HoodieSchema writer = fixedDecimal(8, 15, 2);
+ HoodieSchema reader = fixedDecimal(8, 10, 2);
+ assertFalse(HoodieSchemaTypePromotion.isDecimalWidening(reader, writer));
+ }
+
+ @Test
+ public void testDecimalWideningRejectsIncreasedScaleWithoutRoom() {
+ // integer digits shrink from 8 to 5, so widening is invalid
+ HoodieSchema writer = fixedDecimal(8, 10, 2);
+ HoodieSchema reader = fixedDecimal(8, 10, 5);
+ assertFalse(HoodieSchemaTypePromotion.isDecimalWidening(reader, writer));
+ }
+
+ @Test
+ public void testDecimalWideningRejectsDifferentFixedSize() {
+ HoodieSchema writer = fixedDecimal(8, 10, 2);
+ HoodieSchema reader = fixedDecimal(16, 10, 2);
+ assertFalse(HoodieSchemaTypePromotion.isDecimalWidening(reader, writer));
+ }
+
+ @Test
+ public void testDecimalWideningRejectsNonDecimal() {
+ HoodieSchema decimal = fixedDecimal(8, 10, 2);
+ HoodieSchema plainInt = HoodieSchema.create(HoodieSchemaType.INT);
+ assertFalse(HoodieSchemaTypePromotion.isDecimalWidening(decimal,
plainInt));
+ assertFalse(HoodieSchemaTypePromotion.isDecimalWidening(plainInt,
decimal));
+ }
+
+ private static HoodieSchema fixedDecimal(int size, int precision, int scale)
{
+ return HoodieSchema.createDecimal("FixedDecimal", null, null, precision,
scale, size);
+ }
+}
diff --git
a/hudi-common/src/test/java/org/apache/hudi/metadata/stats/TestValueType.java
b/hudi-common/src/test/java/org/apache/hudi/metadata/stats/TestValueType.java
index ad44db81411a..333ec2ea1ab6 100644
---
a/hudi-common/src/test/java/org/apache/hudi/metadata/stats/TestValueType.java
+++
b/hudi-common/src/test/java/org/apache/hudi/metadata/stats/TestValueType.java
@@ -19,9 +19,31 @@
package org.apache.hudi.metadata.stats;
+import org.apache.hudi.common.schema.HoodieSchema;
+import org.apache.hudi.common.schema.HoodieSchemaType;
+
+import org.apache.avro.generic.GenericData;
+import org.apache.parquet.io.api.Binary;
+import org.apache.parquet.schema.PrimitiveType;
+import org.apache.parquet.schema.Types;
import org.junit.jupiter.api.Test;
+import java.math.BigDecimal;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.sql.Timestamp;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.ZoneOffset;
+import java.util.UUID;
+
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;
+import static org.junit.jupiter.api.Assertions.assertTrue;
public class TestValueType {
@@ -54,4 +76,211 @@ public class TestValueType {
// IN THE FUTURE
assertEquals(21, ValueType.values().length);
}
+
+ @Test
+ public void testFromOrdinalRoundTrips() {
+ for (ValueType type : ValueType.values()) {
+ assertSame(type, ValueType.fromOrdinal(type.ordinal()));
+ }
+ }
+
+ private static Comparable<?> standardize(ValueType type, Object val) {
+ return type.standardizeJavaTypeAndPromote(val,
ValueMetadata.NULL_METADATA);
+ }
+
+ @Test
+ public void testCastToInteger() {
+ assertNull(standardize(ValueType.INT, null));
+ assertEquals(7, standardize(ValueType.INT, 7));
+ assertEquals(1, standardize(ValueType.INT, Boolean.TRUE));
+ assertEquals(0, standardize(ValueType.INT, Boolean.FALSE));
+ // best effort parse from a string representation
+ assertEquals(42, standardize(ValueType.INT, "42"));
+ }
+
+ @Test
+ public void testCastToLong() {
+ assertEquals(5L, standardize(ValueType.LONG, 5));
+ assertEquals(9L, standardize(ValueType.LONG, 9L));
+ assertEquals(1L, standardize(ValueType.LONG, Boolean.TRUE));
+ assertEquals(123L, standardize(ValueType.LONG, "123"));
+ }
+
+ @Test
+ public void testCastToFloatAndDouble() {
+ assertEquals(3.0f, standardize(ValueType.FLOAT, 3));
+ assertEquals(4.0f, standardize(ValueType.FLOAT, 4L));
+ assertEquals(2.5f, standardize(ValueType.FLOAT, 2.5f));
+ assertEquals(1.0f, standardize(ValueType.FLOAT, Boolean.TRUE));
+ assertEquals(6.0d, standardize(ValueType.DOUBLE, 6));
+ assertEquals(7.0d, standardize(ValueType.DOUBLE, 7L));
+ assertEquals(0.0d, standardize(ValueType.DOUBLE, Boolean.FALSE));
+ assertEquals(8.25d, standardize(ValueType.DOUBLE, 8.25d));
+ }
+
+ @Test
+ public void testCastToBoolean() {
+ assertEquals(Boolean.TRUE,
ValueType.BOOLEAN.standardizeJavaTypeAndPromote(true,
ValueMetadata.NULL_METADATA));
+ assertThrows(UnsupportedOperationException.class,
+ () -> ValueType.BOOLEAN.standardizeJavaTypeAndPromote("nope",
ValueMetadata.NULL_METADATA));
+ }
+
+ @Test
+ public void testCastToString() {
+ assertEquals("abc", ValueType.castToString("abc"));
+ assertEquals("11", ValueType.castToString(11));
+ assertEquals("true", ValueType.castToString(Boolean.TRUE));
+ assertEquals("bin", ValueType.castToString(Binary.fromString("bin")));
+ assertThrows(UnsupportedOperationException.class, () ->
ValueType.castToString(new Object()));
+ }
+
+ @Test
+ public void testCastToBytesFromVariousSources() {
+ byte[] raw = "hello".getBytes(StandardCharsets.UTF_8);
+ assertEquals(ByteBuffer.wrap(raw),
ValueType.castToBytes(ByteBuffer.wrap(raw)));
+ assertEquals(ByteBuffer.wrap(raw), ValueType.castToBytes(raw));
+ assertEquals(ByteBuffer.wrap(raw),
ValueType.castToBytes(Binary.fromConstantByteArray(raw)));
+ assertEquals(ByteBuffer.wrap(raw), ValueType.castToBytes("hello"));
+ assertThrows(UnsupportedOperationException.class, () ->
ValueType.castToBytes(new Object()));
+ }
+
+ @Test
+ public void testCastToFixedRejectsString() {
+ byte[] raw = {1, 2, 3};
+ assertEquals(ByteBuffer.wrap(raw), ValueType.castToFixed(raw));
+ // castToFixed, unlike castToBytes, does not accept String
+ assertThrows(UnsupportedOperationException.class, () ->
ValueType.castToFixed("abc"));
+ }
+
+ @Test
+ public void testDecimalRoundTrip() {
+ ValueMetadata.DecimalMetadata meta =
ValueMetadata.DecimalMetadata.create(10, 2);
+ BigDecimal value = new BigDecimal("12.34");
+ // fromDecimal produces the primitive representation, toDecimal reverses it
+ ByteBuffer primitive = ValueType.fromDecimal(value, meta);
+ BigDecimal roundTripped = ValueType.toDecimal(primitive, meta);
+ assertEquals(value, roundTripped);
+ // castToDecimal accepts an already-typed BigDecimal unchanged
+ assertEquals(value, ValueType.castToDecimal(value, meta));
+ // integer input scaled by the metadata scale
+ assertEquals(new BigDecimal("1.00"), ValueType.castToDecimal(100, meta));
+ assertThrows(UnsupportedOperationException.class, () ->
ValueType.castToDecimal(new Object(), meta));
+ }
+
+ @Test
+ public void testUuidRoundTrip() {
+ UUID uuid = UUID.fromString("12345678-1234-1234-1234-1234567890ab");
+ assertEquals(uuid, ValueType.castToUUID(uuid,
ValueMetadata.NULL_METADATA));
+ assertEquals(uuid, ValueType.castToUUID(uuid.toString(),
ValueMetadata.NULL_METADATA));
+ String primitive = ValueType.fromUUID(uuid, ValueMetadata.NULL_METADATA);
+ assertEquals(uuid, ValueType.toUUID(primitive,
ValueMetadata.NULL_METADATA));
+ assertThrows(UnsupportedOperationException.class,
+ () -> ValueType.castToUUID(1, ValueMetadata.NULL_METADATA));
+ }
+
+ @Test
+ public void testDateRoundTrip() {
+ LocalDate date = LocalDate.of(2020, 1, 2);
+ int epochDay = (int) date.toEpochDay();
+ assertEquals(date, ValueType.castToDate(date,
ValueMetadata.NULL_METADATA));
+ assertEquals(date, ValueType.castToDate(epochDay,
ValueMetadata.NULL_METADATA));
+ assertEquals(date, ValueType.castToDate(java.sql.Date.valueOf(date),
ValueMetadata.NULL_METADATA));
+ assertEquals(epochDay, ValueType.fromDate(date,
ValueMetadata.NULL_METADATA));
+ assertEquals(date, ValueType.toDate(epochDay,
ValueMetadata.NULL_METADATA));
+ assertThrows(UnsupportedOperationException.class,
+ () -> ValueType.castToDate("2020-01-02", ValueMetadata.NULL_METADATA));
+ }
+
+ @Test
+ public void testTimeMillisAndMicrosRoundTrip() {
+ LocalTime time = LocalTime.of(1, 2, 3, 4_000_000);
+ int millisOfDay = time.toSecondOfDay() * 1000 + time.getNano() / 1_000_000;
+ assertEquals(time, ValueType.castToTimeMillis(time,
ValueMetadata.NULL_METADATA));
+ assertEquals(time, ValueType.castToTimeMillis(millisOfDay,
ValueMetadata.NULL_METADATA));
+ assertEquals(millisOfDay, ValueType.fromTimeMillis(time,
ValueMetadata.NULL_METADATA));
+ assertEquals(time, ValueType.toTimeMillis(millisOfDay,
ValueMetadata.NULL_METADATA));
+
+ LocalTime microTime = LocalTime.of(4, 5, 6, 7_000);
+ long microsOfDay = microTime.toSecondOfDay() * 1_000_000L +
microTime.getNano() / 1_000;
+ assertEquals(microTime, ValueType.castToTimeMicros(microsOfDay,
ValueMetadata.NULL_METADATA));
+ assertEquals(microsOfDay, ValueType.fromTimeMicros(microTime,
ValueMetadata.NULL_METADATA));
+ }
+
+ @Test
+ public void testTimestampMillisMicrosNanosRoundTrip() {
+ Instant instant = Instant.ofEpochMilli(1_600_000_000_123L);
+ assertEquals(instant, ValueType.castToTimestampMillis(instant,
ValueMetadata.NULL_METADATA));
+ assertEquals(instant,
ValueType.castToTimestampMillis(Timestamp.from(instant),
ValueMetadata.NULL_METADATA));
+ assertEquals(instant,
ValueType.castToTimestampMillis(instant.toEpochMilli(),
ValueMetadata.NULL_METADATA));
+ assertEquals(instant.toEpochMilli(),
ValueType.fromTimestampMillis(instant, ValueMetadata.NULL_METADATA));
+ assertEquals(instant, ValueType.toTimestampMillis(instant.toEpochMilli(),
ValueMetadata.NULL_METADATA));
+
+ Instant micros = Instant.ofEpochSecond(1_600_000_000L, 123_000L);
+ long microVal = ValueType.fromTimestampMicros(micros,
ValueMetadata.NULL_METADATA);
+ assertEquals(micros, ValueType.toTimestampMicros(microVal,
ValueMetadata.NULL_METADATA));
+
+ Instant nanos = Instant.ofEpochSecond(1_600_000_000L, 123_456L);
+ long nanoVal = ValueType.fromTimestampNanos(nanos,
ValueMetadata.NULL_METADATA);
+ assertEquals(nanos, ValueType.toTimestampNanos(nanoVal,
ValueMetadata.NULL_METADATA));
+ }
+
+ @Test
+ public void testLocalTimestampRoundTrip() {
+ LocalDateTime local = LocalDateTime.of(2021, 5, 6, 7, 8, 9, 10_000_000);
+ long millis = ValueType.fromLocalTimestampMillis(local,
ValueMetadata.NULL_METADATA);
+ assertEquals(local, ValueType.toLocalTimestampMillis(millis,
ValueMetadata.NULL_METADATA));
+ assertEquals(local, ValueType.castToLocalTimestampMillis(local,
ValueMetadata.NULL_METADATA));
+ assertEquals(local,
+
ValueType.castToLocalTimestampMillis(local.toInstant(ZoneOffset.UTC).toEpochMilli(),
ValueMetadata.NULL_METADATA));
+
+ LocalDateTime micros = LocalDateTime.of(2021, 5, 6, 7, 8, 9, 11_000);
+ long microVal = ValueType.fromLocalTimestampMicros(micros,
ValueMetadata.NULL_METADATA);
+ assertEquals(micros, ValueType.toLocalTimestampMicros(microVal,
ValueMetadata.NULL_METADATA));
+
+ LocalDateTime nanos = LocalDateTime.of(2021, 5, 6, 7, 8, 9, 12_345);
+ long nanoVal = ValueType.fromLocalTimestampNanos(nanos,
ValueMetadata.NULL_METADATA);
+ assertEquals(nanos, ValueType.toLocalTimestampNanos(nanoVal,
ValueMetadata.NULL_METADATA));
+ }
+
+ @Test
+ public void testFromParquetPrimitiveType() {
+ assertEquals(ValueType.LONG,
valueTypeFor(PrimitiveType.PrimitiveTypeName.INT64));
+ assertEquals(ValueType.INT,
valueTypeFor(PrimitiveType.PrimitiveTypeName.INT32));
+ assertEquals(ValueType.BOOLEAN,
valueTypeFor(PrimitiveType.PrimitiveTypeName.BOOLEAN));
+ assertEquals(ValueType.BYTES,
valueTypeFor(PrimitiveType.PrimitiveTypeName.BINARY));
+ assertEquals(ValueType.FLOAT,
valueTypeFor(PrimitiveType.PrimitiveTypeName.FLOAT));
+ assertEquals(ValueType.DOUBLE,
valueTypeFor(PrimitiveType.PrimitiveTypeName.DOUBLE));
+ }
+
+ private static ValueType valueTypeFor(PrimitiveType.PrimitiveTypeName name) {
+ PrimitiveType type = name ==
PrimitiveType.PrimitiveTypeName.FIXED_LEN_BYTE_ARRAY
+ ? Types.required(name).length(4).named("f")
+ : Types.required(name).named("f");
+ return ValueType.fromParquetPrimitiveType(type);
+ }
+
+ @Test
+ public void testFromSchemaPrimitives() {
+ assertEquals(ValueType.INT,
ValueType.fromSchema(HoodieSchema.create(HoodieSchemaType.INT)));
+ assertEquals(ValueType.LONG,
ValueType.fromSchema(HoodieSchema.create(HoodieSchemaType.LONG)));
+ assertEquals(ValueType.STRING,
ValueType.fromSchema(HoodieSchema.create(HoodieSchemaType.STRING)));
+ assertEquals(ValueType.BOOLEAN,
ValueType.fromSchema(HoodieSchema.create(HoodieSchemaType.BOOLEAN)));
+ assertEquals(ValueType.DATE,
ValueType.fromSchema(HoodieSchema.create(HoodieSchemaType.DATE)));
+ assertEquals(ValueType.UUID,
ValueType.fromSchema(HoodieSchema.create(HoodieSchemaType.UUID)));
+ }
+
+ @Test
+ public void testFromSchemaUnwrapsUnion() {
+ HoodieSchema nullableInt =
HoodieSchema.createNullable(HoodieSchemaType.INT);
+ assertEquals(ValueType.INT, ValueType.fromSchema(nullableInt));
+ }
+
+ @Test
+ public void testCastToBytesFromFixed() {
+ byte[] raw = {9, 8, 7};
+ GenericData.Fixed fixed = new GenericData.Fixed(
+ org.apache.avro.Schema.createFixed("f", null, null, raw.length), raw);
+ assertEquals(ByteBuffer.wrap(raw), ValueType.castToBytes(fixed));
+ assertTrue(ValueType.castToBytes(fixed).hasArray());
+ }
}
diff --git
a/hudi-common/src/test/java/org/apache/hudi/metrics/m3/TestM3ScopeReporterAdaptor.java
b/hudi-common/src/test/java/org/apache/hudi/metrics/m3/TestM3ScopeReporterAdaptor.java
new file mode 100644
index 000000000000..a445e82a7f75
--- /dev/null
+++
b/hudi-common/src/test/java/org/apache/hudi/metrics/m3/TestM3ScopeReporterAdaptor.java
@@ -0,0 +1,163 @@
+/*
+ * 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.metrics.m3;
+
+import com.codahale.metrics.Counter;
+import com.codahale.metrics.Histogram;
+import com.codahale.metrics.Meter;
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.Timer;
+import com.codahale.metrics.UniformReservoir;
+import com.uber.m3.tally.Gauge;
+import com.uber.m3.tally.Scope;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Verifies that {@link M3ScopeReporterAdaptor} maps codahale registry metrics
+ * onto the target m3 tally {@link Scope}. The scope is a mock so that the
exact
+ * counters and gauges reaching it can be asserted without any network I/O.
+ */
+public class TestM3ScopeReporterAdaptor {
+
+ private MetricRegistry registry;
+ private Scope scope;
+ private com.uber.m3.tally.Counter scopeCounter;
+ private Gauge scopeGauge;
+ private M3ScopeReporterAdaptor reporter;
+
+ @BeforeEach
+ public void setUp() {
+ registry = new MetricRegistry();
+ scope = mock(Scope.class);
+ scopeCounter = mock(com.uber.m3.tally.Counter.class);
+ scopeGauge = mock(Gauge.class);
+
when(scope.counter(org.mockito.ArgumentMatchers.anyString())).thenReturn(scopeCounter);
+
when(scope.gauge(org.mockito.ArgumentMatchers.anyString())).thenReturn(scopeGauge);
+ reporter = new M3ScopeReporterAdaptor(registry, scope);
+ }
+
+ @Test
+ public void testCounterIsForwardedToScope() {
+ Counter counter = registry.counter("requests");
+ counter.inc(7);
+
+ reporter.report();
+
+ verify(scope).counter("requests");
+ verify(scopeCounter).inc(7L);
+ }
+
+ @Test
+ public void testGaugeValueIsForwardedToScope() {
+ registry.register("in_flight", (com.codahale.metrics.Gauge<Integer>) () ->
42);
+
+ reporter.report();
+
+ verify(scope).gauge("in_flight");
+ verify(scopeGauge).update(42.0d);
+ }
+
+ @Test
+ public void testHistogramEmitsCountAndSnapshotGauges() {
+ Histogram histogram = new Histogram(new UniformReservoir());
+ registry.register("latency", histogram);
+ histogram.update(10);
+ histogram.update(20);
+
+ reporter.report();
+
+ // count is emitted as its own gauge suffix
+ verify(scope).gauge("latency.count");
+ // the snapshot expands into ten percentile/stat gauges
+ verify(scope).gauge("latency.max");
+ verify(scope).gauge("latency.mean");
+ verify(scope).gauge("latency.min");
+ verify(scope).gauge("latency.stddev");
+ verify(scope).gauge("latency.p50");
+ verify(scope).gauge("latency.p75");
+ verify(scope).gauge("latency.p95");
+ verify(scope).gauge("latency.p98");
+ verify(scope).gauge("latency.p99");
+ verify(scope).gauge("latency.p999");
+ // no bare "latency" gauge, only the suffixed ones
+ verify(scope, never()).gauge("latency");
+ }
+
+ @Test
+ public void testMeterEmitsCountCounterAndRateGauges() {
+ Meter meter = registry.meter("throughput");
+ meter.mark(5);
+
+ reporter.report();
+
+ verify(scope).counter("throughput.count");
+ verify(scopeCounter).inc(5L);
+ verify(scope).gauge("throughput.m1_rate");
+ verify(scope).gauge("throughput.m5_rate");
+ verify(scope).gauge("throughput.m15_rate");
+ verify(scope).gauge("throughput.mean_rate");
+ }
+
+ @Test
+ public void testTimerEmitsBothMeteredAndSnapshotMetrics() {
+ Timer timer = registry.timer("op_time");
+ timer.update(java.time.Duration.ofMillis(3));
+ timer.update(java.time.Duration.ofMillis(9));
+
+ reporter.report();
+
+ // timer reports the metered count as a counter
+ verify(scope).counter("op_time.count");
+ // and the four rate gauges from the metered portion
+ verify(scope).gauge("op_time.m1_rate");
+ verify(scope).gauge("op_time.mean_rate");
+ // plus the full snapshot set from the timer's histogram
+ verify(scope).gauge("op_time.max");
+ verify(scope).gauge("op_time.p99");
+ }
+
+ @Test
+ public void testEmptyRegistryTouchesNothing() {
+ reporter.report();
+
+ verify(scope, never()).counter(org.mockito.ArgumentMatchers.anyString());
+ verify(scope, never()).gauge(org.mockito.ArgumentMatchers.anyString());
+ }
+
+ @Test
+ public void testMultipleGaugesEachMapped() {
+ registry.register("g1", (com.codahale.metrics.Gauge<Long>) () -> 1L);
+ registry.register("g2", (com.codahale.metrics.Gauge<Long>) () -> 2L);
+
+ reporter.report();
+
+ verify(scope).gauge("g1");
+ verify(scope).gauge("g2");
+ verify(scopeGauge).update(1.0d);
+ verify(scopeGauge).update(2.0d);
+ verify(scope,
times(2)).gauge(org.mockito.ArgumentMatchers.startsWith("g"));
+ }
+}