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

Reply via email to