001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018package org.apache.hadoop.hbase; 019 020import static org.junit.jupiter.api.Assertions.fail; 021 022import java.lang.reflect.Constructor; 023import java.lang.reflect.Method; 024import java.time.Duration; 025import java.time.Instant; 026import java.util.Map; 027import java.util.Set; 028import java.util.concurrent.ExecutionException; 029import java.util.concurrent.ExecutorService; 030import java.util.concurrent.Executors; 031import java.util.concurrent.Future; 032import java.util.concurrent.TimeUnit; 033import java.util.concurrent.TimeoutException; 034import org.apache.hadoop.hbase.testclassification.IntegrationTests; 035import org.apache.hadoop.hbase.testclassification.LargeTests; 036import org.apache.hadoop.hbase.testclassification.MediumTests; 037import org.apache.hadoop.hbase.testclassification.SmallTests; 038import org.apache.yetus.audience.InterfaceAudience; 039import org.junit.jupiter.api.extension.AfterAllCallback; 040import org.junit.jupiter.api.extension.BeforeAllCallback; 041import org.junit.jupiter.api.extension.ExtensionContext; 042import org.junit.jupiter.api.extension.ExtensionContext.Store; 043import org.junit.jupiter.api.extension.InvocationInterceptor; 044import org.junit.jupiter.api.extension.ReflectiveInvocationContext; 045import org.junit.platform.commons.JUnitException; 046import org.junit.platform.commons.util.ExceptionUtils; 047import org.slf4j.Logger; 048import org.slf4j.LoggerFactory; 049 050import org.apache.hbase.thirdparty.com.google.common.collect.ImmutableMap; 051import org.apache.hbase.thirdparty.com.google.common.collect.Iterables; 052import org.apache.hbase.thirdparty.com.google.common.collect.Sets; 053import org.apache.hbase.thirdparty.com.google.common.util.concurrent.ThreadFactoryBuilder; 054 055/** 056 * Class test rule implementation for JUnit5. 057 * <p> 058 * It ensures that all JUnit5 tests should have at least one of {@link SmallTests}, 059 * {@link MediumTests}, {@link LargeTests}, {@link IntegrationTests} tags, and set timeout based on 060 * the tag. 061 * <p> 062 * It also controls the timeout for the whole test class running, while the timeout annotation in 063 * JUnit5 can only enforce the timeout for each test method. 064 * <p> 065 * Finally, it also forbid System.exit call in tests. TODO: need to find a new way as 066 * SecurityManager has been removed since Java 21. 067 */ 068@InterfaceAudience.Private 069public class HBaseJupiterExtension 070 implements InvocationInterceptor, BeforeAllCallback, AfterAllCallback { 071 072 private static final Logger LOG = LoggerFactory.getLogger(HBaseJupiterExtension.class); 073 074 private static final SecurityManager securityManager = new TestSecurityManager(); 075 076 private static final ExtensionContext.Namespace NAMESPACE = 077 ExtensionContext.Namespace.create(HBaseJupiterExtension.class); 078 079 private static final Map<String, Duration> TAG_TO_TIMEOUT = 080 ImmutableMap.of(SmallTests.TAG, Duration.ofMinutes(3), MediumTests.TAG, Duration.ofMinutes(6), 081 LargeTests.TAG, Duration.ofMinutes(13), IntegrationTests.TAG, Duration.ZERO); 082 083 private static final String EXECUTOR = "executor"; 084 085 private static final String DEADLINE = "deadline"; 086 087 private Duration pickTimeout(ExtensionContext ctx) { 088 Set<String> timeoutTags = TAG_TO_TIMEOUT.keySet(); 089 Set<String> timeoutTag = Sets.intersection(timeoutTags, ctx.getTags()); 090 if (timeoutTag.isEmpty()) { 091 fail("Test class " + ctx.getDisplayName() + " does not have any of the following scale tags " 092 + timeoutTags); 093 } 094 if (timeoutTag.size() > 1) { 095 fail("Test class " + ctx.getDisplayName() + " has multiple scale tags " + timeoutTag); 096 } 097 return TAG_TO_TIMEOUT.get(Iterables.getOnlyElement(timeoutTag)); 098 } 099 100 @Override 101 public void beforeAll(ExtensionContext ctx) throws Exception { 102 // TODO: remove this usage 103 System.setSecurityManager(securityManager); 104 Duration timeout = pickTimeout(ctx); 105 if (timeout.isZero() || timeout.isNegative()) { 106 LOG.info("No timeout for {}", ctx.getDisplayName()); 107 // zero means no timeout 108 return; 109 } 110 Instant deadline = Instant.now().plus(timeout); 111 LOG.info("Timeout for {} is {}, it should be finished before {}", ctx.getDisplayName(), timeout, 112 deadline); 113 ExecutorService executor = 114 Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setDaemon(true) 115 .setNameFormat("HBase-Test-" + ctx.getDisplayName() + "-Main-Thread").build()); 116 Store store = ctx.getStore(NAMESPACE); 117 store.put(EXECUTOR, executor); 118 store.put(DEADLINE, deadline); 119 } 120 121 @Override 122 public void afterAll(ExtensionContext ctx) throws Exception { 123 Store store = ctx.getStore(NAMESPACE); 124 ExecutorService executor = store.remove(EXECUTOR, ExecutorService.class); 125 if (executor != null) { 126 executor.shutdownNow(); 127 } 128 store.remove(DEADLINE); 129 // reset secutiry manager 130 System.setSecurityManager(null); 131 } 132 133 private <T> T runWithTimeout(Invocation<T> invocation, ExtensionContext ctx) throws Throwable { 134 Store store = ctx.getStore(NAMESPACE); 135 ExecutorService executor = store.get(EXECUTOR, ExecutorService.class); 136 if (executor == null) { 137 return invocation.proceed(); 138 } 139 Instant deadline = store.get(DEADLINE, Instant.class); 140 Instant now = Instant.now(); 141 if (!now.isBefore(deadline)) { 142 fail("Test " + ctx.getDisplayName() + " timed out, deadline is " + deadline); 143 return null; 144 } 145 146 Duration remaining = Duration.between(now, deadline); 147 LOG.info("remaining timeout for {} is {}", ctx.getDisplayName(), remaining); 148 Future<T> future = executor.submit(() -> { 149 try { 150 return invocation.proceed(); 151 } catch (Throwable t) { 152 // follow the same pattern with junit5 153 throw ExceptionUtils.throwAsUncheckedException(t); 154 } 155 }); 156 try { 157 return future.get(remaining.toNanos(), TimeUnit.NANOSECONDS); 158 } catch (InterruptedException e) { 159 Thread.currentThread().interrupt(); 160 fail("Test " + ctx.getDisplayName() + " interrupted"); 161 return null; 162 } catch (ExecutionException e) { 163 throw ExceptionUtils.throwAsUncheckedException(e.getCause()); 164 } catch (TimeoutException e) { 165 printThreadDump(); 166 throw new JUnitException( 167 "Test " + ctx.getDisplayName() + " timed out, deadline is " + deadline, e); 168 } 169 } 170 171 private void printThreadDump() { 172 LOG.info("====> TEST TIMED OUT. PRINTING THREAD DUMP. <===="); 173 LOG.info(TimedOutTestsListener.buildThreadDiagnosticString()); 174 } 175 176 @Override 177 public void interceptBeforeAllMethod(Invocation<Void> invocation, 178 ReflectiveInvocationContext<Method> invocationContext, ExtensionContext extensionContext) 179 throws Throwable { 180 runWithTimeout(invocation, extensionContext); 181 } 182 183 @Override 184 public void interceptBeforeEachMethod(Invocation<Void> invocation, 185 ReflectiveInvocationContext<Method> invocationContext, ExtensionContext extensionContext) 186 throws Throwable { 187 runWithTimeout(invocation, extensionContext); 188 } 189 190 @Override 191 public void interceptTestMethod(Invocation<Void> invocation, 192 ReflectiveInvocationContext<Method> invocationContext, ExtensionContext extensionContext) 193 throws Throwable { 194 runWithTimeout(invocation, extensionContext); 195 } 196 197 @Override 198 public void interceptAfterEachMethod(Invocation<Void> invocation, 199 ReflectiveInvocationContext<Method> invocationContext, ExtensionContext extensionContext) 200 throws Throwable { 201 runWithTimeout(invocation, extensionContext); 202 } 203 204 @Override 205 public void interceptAfterAllMethod(Invocation<Void> invocation, 206 ReflectiveInvocationContext<Method> invocationContext, ExtensionContext extensionContext) 207 throws Throwable { 208 runWithTimeout(invocation, extensionContext); 209 } 210 211 @Override 212 public <T> T interceptTestClassConstructor(Invocation<T> invocation, 213 ReflectiveInvocationContext<Constructor<T>> invocationContext, 214 ExtensionContext extensionContext) throws Throwable { 215 return runWithTimeout(invocation, extensionContext); 216 } 217}