All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.cedarsoft.test.utils.ThreadExtension Maven / Gradle / Ivy

The newest version!
/**
 * Copyright (C) cedarsoft GmbH.
 *
 * Licensed under the GNU General Public License version 3 (the "License")
 * with Classpath Exception; you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 *         http://www.cedarsoft.org/gpl3ce
 *         (GPL 3 with Classpath Exception)
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 3 only, as
 * published by the Free Software Foundation. cedarsoft GmbH designates this
 * particular file as subject to the "Classpath" exception as provided
 * by cedarsoft GmbH in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 3 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 3 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact cedarsoft GmbH, 72810 Gomaringen, Germany,
 * or visit www.cedarsoft.com if you need additional information or
 * have any questions.
 */
package com.cedarsoft.test.utils;

import com.google.common.base.Joiner;
import org.junit.jupiter.api.extension.*;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;


/**
 * Extension that checks whether some threads have been left after the unit test has finished.
 */
public class ThreadExtension implements BeforeEachCallback, AfterEachCallback {
  @Override
  public void beforeEach(ExtensionContext context) throws Exception {
    before();
  }

  @Override
  public void afterEach(ExtensionContext context) throws Exception {
    if (context.getExecutionException().isPresent()) {
      afterFailing();
      return;
    }

    after();
  }

  public static final String STACK_TRACE_ELEMENT_SEPARATOR = "\n\tat ";

  @Nullable
  private final ThreadMatcher ignoredThreadMatcher;

  public ThreadExtension() {
    this(new DefaultThreadMatcher() );
  }

  public ThreadExtension(@Nullable ThreadMatcher ignoredThreadMatcher ) {
    this.ignoredThreadMatcher = ignoredThreadMatcher;
  }

  @Nullable
  private Collection initialThreads;

  private void before() {
    if ( initialThreads != null ) {
      throw new IllegalStateException("initialThreads is not null");
    }

    initialThreads = Thread.getAllStackTraces().keySet();
  }

  @Nonnull
  public Collection getInitialThreads() {
    if ( initialThreads == null ) {
      throw new IllegalStateException( "not initialized yet" );
    }
    return Collections.unmodifiableCollection( initialThreads );
  }

  private void afterFailing() {
    Set remainingThreads = getRemainingThreads();
    if ( !remainingThreads.isEmpty() ) {
      System.err.print( "Some threads have been left:\n" + buildMessage( remainingThreads ) );
    }

    initialThreads = null;
  }

  private void after() {
    try {
      Set remainingThreads = getRemainingThreads();
      if (!remainingThreads.isEmpty()) {
        System.err.println("--> " + "Some threads have been left:\n" + buildMessage(remainingThreads));
        throw new IllegalStateException("Some threads have been left:\n" + buildMessage(remainingThreads));
      }
    } finally {
      initialThreads = null;
    }
  }

  @Nonnull
  public Set getRemainingThreads() {
    if (initialThreads == null) {
      throw new IllegalStateException("initialThreads is null");
    }
    Collection threadsNow = Thread.getAllStackTraces().keySet();

    Set remainingThreads = new HashSet(threadsNow);
    remainingThreads.removeAll(initialThreads);

    for (Iterator iterator = remainingThreads.iterator(); iterator.hasNext(); ) {
      Thread remainingThread = iterator.next();
      if ( !remainingThread.isAlive() ) {
        iterator.remove();
        continue;
      }

      //Ignore the threads
      if (this.ignoredThreadMatcher != null && ignoredThreadMatcher.shallIgnore(remainingThread)) {
        iterator.remove();
        continue;
      }

      //Wait for a little bit, sometimes the threads die off
      for (int i = 0; i < 10; i++) {
        try {
          Thread.sleep(10);
        } catch (InterruptedException ignore) {
        }

        //Second try
        if (!remainingThread.isAlive()) {
          iterator.remove();
          break;
        }
      }
    }
    return remainingThreads;
  }

  @Nonnull
  private String buildMessage(@Nonnull Set remainingThreads ) {
    StringBuilder builder = new StringBuilder();

    builder.append( "// Remaining Threads:" ).append( "\n" );
    builder.append( "-----------------------" ).append( "\n" );
    for ( Thread remainingThread : remainingThreads ) {
      builder.append( "---" );
      builder.append( "\n" );
      builder.append( remainingThread );
      builder.append( STACK_TRACE_ELEMENT_SEPARATOR );
      builder.append(Joiner.on(STACK_TRACE_ELEMENT_SEPARATOR ).join(remainingThread.getStackTrace() ) );
      builder.append("\n" );
    }
    builder.append("-----------------------" ).append("\n" );

    return builder.toString();
  }

  public interface ThreadMatcher {
    boolean shallIgnore(@Nonnull Thread remainingThread );
  }

  /**
   * Default implementation that ignore several known threads.
   */
  public static class DefaultThreadMatcher implements ThreadMatcher {
    @Override
    public boolean shallIgnore(@Nonnull Thread remainingThread) {
      @Nullable ThreadGroup threadGroup = remainingThread.getThreadGroup();
      if ( threadGroup == null ) {
        //this means the thread has died
        return true;
      }
      String threadGroupName = threadGroup.getName();
      String threadName = remainingThread.getName();

      if ((threadGroupName.equals("system") &&
             threadName.equals("Keep-Alive-Timer"))
            ||
            (threadGroupName.equals("system") &&
               threadName.equals("process reaper"))
            ||
            (threadGroupName.equals("system") &&
               threadName.equals("Keep-Alive-SocketCleaner"))
            ||
            (threadGroupName.equals("system") &&
               threadName.equals("Java2D Disposer"))
            ||
            threadName.startsWith("AWT-")
            ||
            (threadGroupName.equals("main") &&
               threadName.startsWith("QuantumRenderer"))
            ||
            (threadGroupName.equals("InnocuousThreadGroup") &&
               threadName.startsWith("Keep-Alive-Timer"))
      ) {
        return true;
      }

      //Special check for awaitility - this lib leaves one thread open for about 100ms
      for (StackTraceElement stackTraceElement : remainingThread.getStackTrace()) {
        if (stackTraceElement.getClassName().equals("org.awaitility.core.ConditionAwaiter$1")) {
          if (stackTraceElement.getMethodName().equals("run")) {
            return true;
          }
        }
      }

      return false;
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy