This is an automated email from the ASF dual-hosted git repository.

zhangduo pushed a commit to branch branch-2.5
in repository https://gitbox.apache.org/repos/asf/hbase.git


The following commit(s) were added to refs/heads/branch-2.5 by this push:
     new f02b20fd313 HBASE-29576 Replicate HBaseClassTestRule functionality for 
Junit 5 (#7331)
f02b20fd313 is described below

commit f02b20fd313b8dc5ba4299d73be2e5e37c1f428a
Author: Duo Zhang <zhang...@apache.org>
AuthorDate: Mon Sep 22 16:11:48 2025 +0800

    HBASE-29576 Replicate HBaseClassTestRule functionality for Junit 5 (#7331)
    
    Signed-off-by: Istvan Toth <st...@apache.org>
    (cherry picked from commit 1cd9f29786127f4a6935f4e034d94ea083b12964)
---
 .../apache/hadoop/hbase/HBaseJupiterExtension.java | 212 +++++++++++++++++++++
 .../hadoop/hbase/TestJUnit5TagConstants.java       |   4 -
 .../org.junit.jupiter.api.extension.Extension      |  16 ++
 .../apache/hadoop/hbase/http/TestLdapAdminACL.java |   3 -
 .../hadoop/hbase/http/TestLdapHttpServer.java      |   3 -
 pom.xml                                            |   1 +
 6 files changed, 229 insertions(+), 10 deletions(-)

diff --git 
a/hbase-common/src/test/java/org/apache/hadoop/hbase/HBaseJupiterExtension.java 
b/hbase-common/src/test/java/org/apache/hadoop/hbase/HBaseJupiterExtension.java
new file mode 100644
index 00000000000..ff2ad14fe76
--- /dev/null
+++ 
b/hbase-common/src/test/java/org/apache/hadoop/hbase/HBaseJupiterExtension.java
@@ -0,0 +1,212 @@
+/*
+ * 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.hadoop.hbase;
+
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import org.apache.hadoop.hbase.testclassification.IntegrationTests;
+import org.apache.hadoop.hbase.testclassification.LargeTests;
+import org.apache.hadoop.hbase.testclassification.MediumTests;
+import org.apache.hadoop.hbase.testclassification.SmallTests;
+import org.apache.yetus.audience.InterfaceAudience;
+import org.junit.jupiter.api.extension.AfterAllCallback;
+import org.junit.jupiter.api.extension.BeforeAllCallback;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.ExtensionContext.Store;
+import org.junit.jupiter.api.extension.InvocationInterceptor;
+import org.junit.jupiter.api.extension.ReflectiveInvocationContext;
+import org.junit.platform.commons.JUnitException;
+import org.junit.platform.commons.util.ExceptionUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.hbase.thirdparty.com.google.common.collect.ImmutableMap;
+import org.apache.hbase.thirdparty.com.google.common.collect.Iterables;
+import org.apache.hbase.thirdparty.com.google.common.collect.Sets;
+import 
org.apache.hbase.thirdparty.com.google.common.util.concurrent.ThreadFactoryBuilder;
+
+/**
+ * Class test rule implementation for JUnit5.
+ * <p>
+ * It ensures that all JUnit5 tests should have at least one of {@link 
SmallTests},
+ * {@link MediumTests}, {@link LargeTests}, {@link IntegrationTests} tags, and 
set timeout based on
+ * the tag.
+ * <p>
+ * It also controls the timeout for the whole test class running, while the 
timeout annotation in
+ * JUnit5 can only enforce the timeout for each test method.
+ * <p>
+ * Finally, it also forbid System.exit call in tests. TODO: need to find a new 
way as
+ * SecurityManager has been removed since Java 21.
+ */
+@InterfaceAudience.Private
+public class HBaseJupiterExtension
+  implements InvocationInterceptor, BeforeAllCallback, AfterAllCallback {
+
+  private static final Logger LOG = 
LoggerFactory.getLogger(HBaseJupiterExtension.class);
+
+  private static final SecurityManager securityManager = new 
TestSecurityManager();
+
+  private static final ExtensionContext.Namespace NAMESPACE =
+    ExtensionContext.Namespace.create(HBaseJupiterExtension.class);
+
+  private static final Map<String, Duration> TAG_TO_TIMEOUT =
+    ImmutableMap.of(SmallTests.TAG, Duration.ofMinutes(3), MediumTests.TAG, 
Duration.ofMinutes(6),
+      LargeTests.TAG, Duration.ofMinutes(13), IntegrationTests.TAG, 
Duration.ZERO);
+
+  private static final String EXECUTOR = "executor";
+
+  private static final String DEADLINE = "deadline";
+
+  private Duration pickTimeout(ExtensionContext ctx) {
+    Set<String> timeoutTags = TAG_TO_TIMEOUT.keySet();
+    Set<String> timeoutTag = Sets.intersection(timeoutTags, ctx.getTags());
+    if (timeoutTag.isEmpty()) {
+      fail("Test class " + ctx.getDisplayName() + " does not have any of the 
following scale tags "
+        + timeoutTags);
+    }
+    if (timeoutTag.size() > 1) {
+      fail("Test class " + ctx.getDisplayName() + " has multiple scale tags " 
+ timeoutTag);
+    }
+    return TAG_TO_TIMEOUT.get(Iterables.getOnlyElement(timeoutTag));
+  }
+
+  @Override
+  public void beforeAll(ExtensionContext ctx) throws Exception {
+    // TODO: remove this usage
+    System.setSecurityManager(securityManager);
+    Duration timeout = pickTimeout(ctx);
+    if (timeout.isZero() || timeout.isNegative()) {
+      LOG.info("No timeout for {}", ctx.getDisplayName());
+      // zero means no timeout
+      return;
+    }
+    Instant deadline = Instant.now().plus(timeout);
+    LOG.info("Timeout for {} is {}, it should be finished before {}", 
ctx.getDisplayName(), timeout,
+      deadline);
+    ExecutorService executor =
+      Executors.newSingleThreadExecutor(new 
ThreadFactoryBuilder().setDaemon(true)
+        .setNameFormat("HBase-Test-" + ctx.getDisplayName() + 
"-Main-Thread").build());
+    Store store = ctx.getStore(NAMESPACE);
+    store.put(EXECUTOR, executor);
+    store.put(DEADLINE, deadline);
+  }
+
+  @Override
+  public void afterAll(ExtensionContext ctx) throws Exception {
+    Store store = ctx.getStore(NAMESPACE);
+    ExecutorService executor = store.remove(EXECUTOR, ExecutorService.class);
+    if (executor != null) {
+      executor.shutdownNow();
+    }
+    store.remove(DEADLINE);
+    // reset secutiry manager
+    System.setSecurityManager(null);
+  }
+
+  private <T> T runWithTimeout(Invocation<T> invocation, ExtensionContext ctx) 
throws Throwable {
+    Store store = ctx.getStore(NAMESPACE);
+    ExecutorService executor = store.get(EXECUTOR, ExecutorService.class);
+    if (executor == null) {
+      return invocation.proceed();
+    }
+    Instant deadline = store.get(DEADLINE, Instant.class);
+    Instant now = Instant.now();
+    if (!now.isBefore(deadline)) {
+      fail("Test " + ctx.getDisplayName() + " timed out, deadline is " + 
deadline);
+      return null;
+    }
+
+    Duration remaining = Duration.between(now, deadline);
+    LOG.info("remaining timeout for {} is {}", ctx.getDisplayName(), 
remaining);
+    Future<T> future = executor.submit(() -> {
+      try {
+        return invocation.proceed();
+      } catch (Throwable t) {
+        // follow the same pattern with junit5
+        throw ExceptionUtils.throwAsUncheckedException(t);
+      }
+    });
+    try {
+      return future.get(remaining.toNanos(), TimeUnit.NANOSECONDS);
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      fail("Test " + ctx.getDisplayName() + " interrupted");
+      return null;
+    } catch (ExecutionException e) {
+      throw ExceptionUtils.throwAsUncheckedException(e.getCause());
+    } catch (TimeoutException e) {
+
+      throw new JUnitException(
+        "Test " + ctx.getDisplayName() + " timed out, deadline is " + 
deadline, e);
+    }
+  }
+
+  @Override
+  public void interceptBeforeAllMethod(Invocation<Void> invocation,
+    ReflectiveInvocationContext<Method> invocationContext, ExtensionContext 
extensionContext)
+    throws Throwable {
+    runWithTimeout(invocation, extensionContext);
+  }
+
+  @Override
+  public void interceptBeforeEachMethod(Invocation<Void> invocation,
+    ReflectiveInvocationContext<Method> invocationContext, ExtensionContext 
extensionContext)
+    throws Throwable {
+    runWithTimeout(invocation, extensionContext);
+  }
+
+  @Override
+  public void interceptTestMethod(Invocation<Void> invocation,
+    ReflectiveInvocationContext<Method> invocationContext, ExtensionContext 
extensionContext)
+    throws Throwable {
+    runWithTimeout(invocation, extensionContext);
+  }
+
+  @Override
+  public void interceptAfterEachMethod(Invocation<Void> invocation,
+    ReflectiveInvocationContext<Method> invocationContext, ExtensionContext 
extensionContext)
+    throws Throwable {
+    runWithTimeout(invocation, extensionContext);
+  }
+
+  @Override
+  public void interceptAfterAllMethod(Invocation<Void> invocation,
+    ReflectiveInvocationContext<Method> invocationContext, ExtensionContext 
extensionContext)
+    throws Throwable {
+    runWithTimeout(invocation, extensionContext);
+  }
+
+  @Override
+  public <T> T interceptTestClassConstructor(Invocation<T> invocation,
+    ReflectiveInvocationContext<Constructor<T>> invocationContext,
+    ExtensionContext extensionContext) throws Throwable {
+    return runWithTimeout(invocation, extensionContext);
+  }
+}
diff --git 
a/hbase-common/src/test/java/org/apache/hadoop/hbase/TestJUnit5TagConstants.java
 
b/hbase-common/src/test/java/org/apache/hadoop/hbase/TestJUnit5TagConstants.java
index 43607e17181..3e30b388ab2 100644
--- 
a/hbase-common/src/test/java/org/apache/hadoop/hbase/TestJUnit5TagConstants.java
+++ 
b/hbase-common/src/test/java/org/apache/hadoop/hbase/TestJUnit5TagConstants.java
@@ -20,21 +20,17 @@ package org.apache.hadoop.hbase;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
 import java.lang.reflect.Field;
-import java.util.concurrent.TimeUnit;
 import org.apache.hadoop.hbase.testclassification.ClientTests;
 import org.apache.hadoop.hbase.testclassification.MiscTests;
 import org.apache.hadoop.hbase.testclassification.SmallTests;
 import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.Timeout;
 
 /**
  * Verify that the values are all correct.
  */
 @Tag(MiscTests.TAG)
 @Tag(SmallTests.TAG)
-// TODO: this is the timeout for each method, not the whole class
-@Timeout(value = 1, unit = TimeUnit.MINUTES)
 public class TestJUnit5TagConstants {
 
   @Test
diff --git 
a/hbase-common/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension
 
b/hbase-common/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension
new file mode 100644
index 00000000000..0cb8a35a1ee
--- /dev/null
+++ 
b/hbase-common/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension
@@ -0,0 +1,16 @@
+# 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.
+org.apache.hadoop.hbase.HBaseJupiterExtension
diff --git 
a/hbase-http/src/test/java/org/apache/hadoop/hbase/http/TestLdapAdminACL.java 
b/hbase-http/src/test/java/org/apache/hadoop/hbase/http/TestLdapAdminACL.java
index c4fd208fa7c..91a3321bdfc 100644
--- 
a/hbase-http/src/test/java/org/apache/hadoop/hbase/http/TestLdapAdminACL.java
+++ 
b/hbase-http/src/test/java/org/apache/hadoop/hbase/http/TestLdapAdminACL.java
@@ -21,7 +21,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 
 import java.io.IOException;
 import java.net.HttpURLConnection;
-import java.util.concurrent.TimeUnit;
 import org.apache.directory.server.annotations.CreateLdapServer;
 import org.apache.directory.server.annotations.CreateTransport;
 import org.apache.directory.server.core.annotations.ApplyLdifs;
@@ -36,7 +35,6 @@ import org.apache.hadoop.hbase.testclassification.SmallTests;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.Timeout;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -56,7 +54,6 @@ import org.slf4j.LoggerFactory;
 
   "dn: uid=jdoe," + LdapConstants.LDAP_BASE_DN, "cn: John Doe", "sn: Doe",
   "objectClass: inetOrgPerson", "uid: jdoe", "userPassword: secure123" })
-@Timeout(value = 1, unit = TimeUnit.MINUTES)
 public class TestLdapAdminACL extends LdapServerTestBase {
 
   private static final Logger LOG = 
LoggerFactory.getLogger(TestLdapAdminACL.class);
diff --git 
a/hbase-http/src/test/java/org/apache/hadoop/hbase/http/TestLdapHttpServer.java 
b/hbase-http/src/test/java/org/apache/hadoop/hbase/http/TestLdapHttpServer.java
index 9faa8dc49fb..c4936513fb3 100644
--- 
a/hbase-http/src/test/java/org/apache/hadoop/hbase/http/TestLdapHttpServer.java
+++ 
b/hbase-http/src/test/java/org/apache/hadoop/hbase/http/TestLdapHttpServer.java
@@ -21,7 +21,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 
 import java.io.IOException;
 import java.net.HttpURLConnection;
-import java.util.concurrent.TimeUnit;
 import org.apache.directory.server.annotations.CreateLdapServer;
 import org.apache.directory.server.annotations.CreateTransport;
 import org.apache.directory.server.core.annotations.ApplyLdifs;
@@ -32,7 +31,6 @@ import org.apache.hadoop.hbase.testclassification.MiscTests;
 import org.apache.hadoop.hbase.testclassification.SmallTests;
 import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.Timeout;
 
 /**
  * Test class for LDAP authentication on the HttpServer.
@@ -47,7 +45,6 @@ import org.junit.jupiter.api.Timeout;
           + "dc: example\n" + "objectClass: top\n" + "objectClass: 
domain\n\n")) })
 @ApplyLdifs({ "dn: uid=bjones," + LdapConstants.LDAP_BASE_DN, "cn: Bob Jones", 
"sn: Jones",
   "objectClass: inetOrgPerson", "uid: bjones", "userPassword: p@ssw0rd" })
-@Timeout(value = 1, unit = TimeUnit.MINUTES)
 public class TestLdapHttpServer extends LdapServerTestBase {
 
   private static final String BJONES_CREDENTIALS = "bjones:p@ssw0rd";
diff --git a/pom.xml b/pom.xml
index 2d16693fe1d..02081c4732d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1701,6 +1701,7 @@
                 <name>listener</name>
                 
<value>org.apache.hadoop.hbase.TimedOutTestsListener,org.apache.hadoop.hbase.HBaseClassTestRuleChecker,org.apache.hadoop.hbase.ResourceCheckerJUnitListener</value>
               </property>
+              
<configurationParameters>junit.jupiter.extensions.autodetection.enabled=true</configurationParameters>
             </properties>
           </configuration>
           <executions>

Reply via email to