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}