diff --git a/spring-test-smart-context/src/main/java/com/github/seregamorph/testsmartcontext/SmartDirtiesContextTestExecutionListener.java b/spring-test-smart-context/src/main/java/com/github/seregamorph/testsmartcontext/SmartDirtiesContextTestExecutionListener.java index 4915751..296b862 100644 --- a/spring-test-smart-context/src/main/java/com/github/seregamorph/testsmartcontext/SmartDirtiesContextTestExecutionListener.java +++ b/spring-test-smart-context/src/main/java/com/github/seregamorph/testsmartcontext/SmartDirtiesContextTestExecutionListener.java @@ -2,6 +2,7 @@ import static com.github.seregamorph.testsmartcontext.SmartDirtiesTestsSupport.isInnerClass; +import com.github.seregamorph.testsmartcontext.leakage.ResourceLeakageManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.test.context.TestContext; @@ -28,28 +29,38 @@ public class SmartDirtiesContextTestExecutionListener extends AbstractTestExecut @Override public int getOrder() { - // DirtiesContextTestExecutionListener.getOrder() + 1 + // DirtiesContextTestExecutionListener.getOrder() + 10 //noinspection MagicNumber - return 3001; + return 3010; } @Override public void beforeTestClass(TestContext testContext) { + Class testClass = testContext.getTestClass(); // stack Nested classes CurrentTestContext.pushCurrentTestClass(testContext.getTestClass()); - Class testClass = testContext.getTestClass(); if (isInnerClass(testClass)) { SmartDirtiesTestsSupport.verifyInnerClass(testClass); } + + ResourceLeakageManager leakageManager = ResourceLeakageManager.getInstance(); + if (SmartDirtiesTestsSupport.isFirstClassPerConfig(testClass)) { + logger.info("firstClassPerConfig {}", testClass.getName()); + leakageManager.handleBeforeClassGroup(); + } + leakageManager.handleBeforeClass(testClass); } @Override public void afterTestClass(TestContext testContext) { try { Class testClass = testContext.getTestClass(); + ResourceLeakageManager leakageManager = ResourceLeakageManager.getInstance(); + leakageManager.handleAfterClass(testClass); if (SmartDirtiesTestsSupport.isLastClassPerConfig(testClass)) { logger.info("markDirty (closing context) after {}", testClass.getName()); testContext.markApplicationContextDirty(null); + leakageManager.handleAfterClassGroup(testClass); } else { logger.debug("Reusing context after {}", testClass.getName()); } diff --git a/spring-test-smart-context/src/main/java/com/github/seregamorph/testsmartcontext/leakage/HeapResourceLeakageDetector.java b/spring-test-smart-context/src/main/java/com/github/seregamorph/testsmartcontext/leakage/HeapResourceLeakageDetector.java new file mode 100644 index 0000000..0e285ab --- /dev/null +++ b/spring-test-smart-context/src/main/java/com/github/seregamorph/testsmartcontext/leakage/HeapResourceLeakageDetector.java @@ -0,0 +1,31 @@ +package com.github.seregamorph.testsmartcontext.leakage; + +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryMXBean; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * + * @author Sergey Chernov + */ +public class HeapResourceLeakageDetector extends ResourceLeakageDetector { + + private final MemoryMXBean memoryMXBean; + + public HeapResourceLeakageDetector() { + super(Arrays.asList("committed", "used")); + this.memoryMXBean = ManagementFactory.getMemoryMXBean(); + } + + @Override + public Map getIndicators() { + Map map = new HashMap<>(); + map.put("committed", memoryMXBean.getHeapMemoryUsage().getCommitted()); + map.put("used", memoryMXBean.getHeapMemoryUsage().getUsed()); +// map.put("init", memoryMXBean.getHeapMemoryUsage().getInit()); +// map.put("max", memoryMXBean.getHeapMemoryUsage().getMax()); + return map; + } +} diff --git a/spring-test-smart-context/src/main/java/com/github/seregamorph/testsmartcontext/leakage/ResourceLeakageCsvLogWriter.java b/spring-test-smart-context/src/main/java/com/github/seregamorph/testsmartcontext/leakage/ResourceLeakageCsvLogWriter.java new file mode 100644 index 0000000..346da6b --- /dev/null +++ b/spring-test-smart-context/src/main/java/com/github/seregamorph/testsmartcontext/leakage/ResourceLeakageCsvLogWriter.java @@ -0,0 +1,64 @@ +package com.github.seregamorph.testsmartcontext.leakage; + +import java.io.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * + * @author Sergey Chernov + */ +public class ResourceLeakageCsvLogWriter extends ResourceLeakageLogWriter { + + private final PrintWriter out; + private final List headers; + + public ResourceLeakageCsvLogWriter(File outputFile, List headers) { + try { + FileOutputStream fileOutputStream = new FileOutputStream(outputFile, false); + out = new PrintWriter(new OutputStreamWriter(fileOutputStream, UTF_8), true); + } catch (FileNotFoundException e) { + throw new UncheckedIOException(e); + } + + this.headers = new ArrayList<>(headers); + StringBuilder fileHeader = new StringBuilder("Timestamp,testClass,event,testGroup,test"); + for (String header : headers) { + fileHeader.append(",").append(header); + } + out.println(fileHeader); + } + + @Override + public void write( + Map indicators, + Class testClass, + String event, + int testGroupNumber, + int testNumber + ) { + String timestamp = getTimestamp(); + StringBuilder line = new StringBuilder(timestamp) + .append(",").append(testClass.getSimpleName()) + .append(",").append(event) + .append(",").append(testGroupNumber) + .append(",").append(testNumber); + for (String header : headers) { + Long value = indicators.get(header); + if (value == null) { + line.append(","); + } else { + line.append(",").append(value); + } + } + out.println(line); + } + + @Override + public void close() { + out.close(); + } +} diff --git a/spring-test-smart-context/src/main/java/com/github/seregamorph/testsmartcontext/leakage/ResourceLeakageDetector.java b/spring-test-smart-context/src/main/java/com/github/seregamorph/testsmartcontext/leakage/ResourceLeakageDetector.java new file mode 100644 index 0000000..92c2924 --- /dev/null +++ b/spring-test-smart-context/src/main/java/com/github/seregamorph/testsmartcontext/leakage/ResourceLeakageDetector.java @@ -0,0 +1,51 @@ +package com.github.seregamorph.testsmartcontext.leakage; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/* +threads +docker containers +heap memory +opened files +opened sockets +loaded classes +CPU history + */ +/** + * + * @author Sergey Chernov + */ +public abstract class ResourceLeakageDetector { + + private final List indicatorKeys; + + List> testClasses; + + protected ResourceLeakageDetector(List indicatorKeys) { + this.indicatorKeys = Collections.unmodifiableList(new ArrayList<>(indicatorKeys)); + } + + public final List getIndicatorKeys() { + return indicatorKeys; + } + + public abstract Map getIndicators(); + + public void handleBeforeClassGroup() { + this.testClasses = new ArrayList<>(); + } + + public void handleAfterClass(Class testClass) { + // testClasses can be null in case if a single test is executed + if (this.testClasses != null) { + this.testClasses.add(testClass); + } + } + + public void handleAfterClassGroup() { + this.testClasses = null; + } +} diff --git a/spring-test-smart-context/src/main/java/com/github/seregamorph/testsmartcontext/leakage/ResourceLeakageLogWriter.java b/spring-test-smart-context/src/main/java/com/github/seregamorph/testsmartcontext/leakage/ResourceLeakageLogWriter.java new file mode 100644 index 0000000..baf1c8f --- /dev/null +++ b/spring-test-smart-context/src/main/java/com/github/seregamorph/testsmartcontext/leakage/ResourceLeakageLogWriter.java @@ -0,0 +1,36 @@ +package com.github.seregamorph.testsmartcontext.leakage; + +import java.io.Closeable; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * + * @author Sergey Chernov + */ +public abstract class ResourceLeakageLogWriter implements Closeable { + + private final long startNanoTime = System.nanoTime(); + + public abstract void write( + Map indicators, + Class testClass, + String event, + int testGroupNumber, + int testNumber + ); + + protected String getTimestamp() { + long now = System.nanoTime(); + long totalSeconds = TimeUnit.NANOSECONDS.toSeconds(now - startNanoTime); + + return formatTimestamp(totalSeconds); + } + + static String formatTimestamp(long totalSeconds) { + long seconds = totalSeconds % 60; + long minutes = (totalSeconds = totalSeconds / 60) % 60; + long hours = totalSeconds / 60; + return hours + ":" + String.format("%02d", minutes) + ":" + String.format("%02d", seconds); + } +} diff --git a/spring-test-smart-context/src/main/java/com/github/seregamorph/testsmartcontext/leakage/ResourceLeakageManager.java b/spring-test-smart-context/src/main/java/com/github/seregamorph/testsmartcontext/leakage/ResourceLeakageManager.java new file mode 100644 index 0000000..155737e --- /dev/null +++ b/spring-test-smart-context/src/main/java/com/github/seregamorph/testsmartcontext/leakage/ResourceLeakageManager.java @@ -0,0 +1,95 @@ +package com.github.seregamorph.testsmartcontext.leakage; + +import org.springframework.lang.Nullable; + +import java.io.File; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +/** + * + * @author Sergey Chernov + */ +public class ResourceLeakageManager { + + private final AtomicInteger testGroupNumber = new AtomicInteger(); + private final AtomicInteger testNumber = new AtomicInteger(); + + private final List resourceLeakageDetectors; + @Nullable + private final ResourceLeakageLogWriter resourceLeakageLogWriter; + + private static final ResourceLeakageManager instance = initInstance(); + + private static ResourceLeakageManager initInstance() { + return new ResourceLeakageManager(); + } + + private ResourceLeakageManager() { + // todo service discovery with priority + this(Arrays.asList( + new ThreadsResourceLeakageDetector(), + new HeapResourceLeakageDetector() + )); + } + + private ResourceLeakageManager(List resourceLeakageDetectors) { + /*@Nullable*/ + File reportsBaseDir = ResourceLeakageUtils.getReportsBaseDir(); + + this.resourceLeakageDetectors = resourceLeakageDetectors; + if (reportsBaseDir == null) { + this.resourceLeakageLogWriter = null; + } else { + File outputFile = new File(reportsBaseDir, "report.csv"); + List headers = resourceLeakageDetectors.stream() + .flatMap(detector -> detector.getIndicatorKeys().stream()) + .collect(Collectors.toList()); + resourceLeakageLogWriter = new ResourceLeakageCsvLogWriter(outputFile, headers); + } + } + + public static ResourceLeakageManager getInstance() { + return instance; + } + + public void handleBeforeClassGroup() { + testGroupNumber.incrementAndGet(); + resourceLeakageDetectors.forEach(ResourceLeakageDetector::handleBeforeClassGroup); + } + + public void handleBeforeClass(Class testClass) { + testNumber.incrementAndGet(); + logIndicators(testClass, "BC"); + } + + public void handleAfterClass(Class testClass) { + logIndicators(testClass, "AC"); + for (ResourceLeakageDetector resourceLeakageDetector : resourceLeakageDetectors) { + resourceLeakageDetector.handleAfterClass(testClass); + } + } + + public void handleAfterClassGroup(Class testClass) { + if (Boolean.getBoolean("testsmartcontext.handleAfterClassGroup.gc")) { + System.gc(); + } + logIndicators(testClass, "ACG"); + for (ResourceLeakageDetector resourceLeakageDetector : resourceLeakageDetectors) { + resourceLeakageDetector.handleAfterClassGroup(); + } + } + + private void logIndicators(Class testClass, String event) { + if (resourceLeakageLogWriter != null) { + Map indicators = new HashMap<>(); + resourceLeakageDetectors.forEach(detector -> indicators.putAll(detector.getIndicators())); + resourceLeakageLogWriter.write(indicators, testClass, event, + testGroupNumber.get(), testNumber.get()); + } + } +} diff --git a/spring-test-smart-context/src/main/java/com/github/seregamorph/testsmartcontext/leakage/ResourceLeakageUtils.java b/spring-test-smart-context/src/main/java/com/github/seregamorph/testsmartcontext/leakage/ResourceLeakageUtils.java new file mode 100644 index 0000000..6509fb0 --- /dev/null +++ b/spring-test-smart-context/src/main/java/com/github/seregamorph/testsmartcontext/leakage/ResourceLeakageUtils.java @@ -0,0 +1,32 @@ +package com.github.seregamorph.testsmartcontext.leakage; + +import java.io.File; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.lang.Nullable; + +/** + * + * @author Sergey Chernov + */ +final class ResourceLeakageUtils { + + private static final Logger logger = LoggerFactory.getLogger(ResourceLeakageUtils.class); + + @Nullable + static File getReportsBaseDir() { + // todo target + // "basedir" is provided by Maven, it's module root + String basedirProperty = System.getProperty("basedir"); + if (basedirProperty == null) { + return null; + } + + File basedir = new File(basedirProperty, "leakage-detector"); + if ((basedir.mkdir() || basedir.exists()) && basedir.isDirectory()) { + return basedir; + } + logger.warn("Failed to create {}", basedir); + return null; + } +} diff --git a/spring-test-smart-context/src/main/java/com/github/seregamorph/testsmartcontext/leakage/ThreadsResourceLeakageDetector.java b/spring-test-smart-context/src/main/java/com/github/seregamorph/testsmartcontext/leakage/ThreadsResourceLeakageDetector.java new file mode 100644 index 0000000..dfcd6a1 --- /dev/null +++ b/spring-test-smart-context/src/main/java/com/github/seregamorph/testsmartcontext/leakage/ThreadsResourceLeakageDetector.java @@ -0,0 +1,152 @@ +package com.github.seregamorph.testsmartcontext.leakage; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.io.Closeable; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.UncheckedIOException; +import java.lang.management.ManagementFactory; +import java.lang.management.ThreadInfo; +import java.lang.management.ThreadMXBean; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.lang.Nullable; + +/** + * @author Sergey Chernov + */ +public class ThreadsResourceLeakageDetector extends ResourceLeakageDetector implements Closeable { + + private static final Logger logger = LoggerFactory.getLogger(ThreadsResourceLeakageDetector.class); + + // todo configurable + private static final boolean CHECK_THREADS_TWICE = true; + + private final ThreadMXBean threadMXBean; + @Nullable + private final PrintWriter leakageOut; + + private volatile Set threadIdsBeforeClassGroup; + + public ThreadsResourceLeakageDetector() { + super(Arrays.asList("threads", "daemonThreads")); + threadMXBean = ManagementFactory.getThreadMXBean(); + + /*@Nullable*/ + File reportsBaseDir = ResourceLeakageUtils.getReportsBaseDir(); + if (reportsBaseDir == null) { + leakageOut = null; + } else { + File file = new File(reportsBaseDir, "threads-leakage.txt"); + FileOutputStream fileOutputStream; + try { + fileOutputStream = new FileOutputStream(file, false); + } catch (FileNotFoundException e) { + throw new UncheckedIOException(e); + } + leakageOut = new PrintWriter(new OutputStreamWriter(fileOutputStream, UTF_8), true); + } + } + + @Override + public Map getIndicators() { + Map map = new HashMap<>(); + map.put("threads", (long) threadMXBean.getThreadCount()); + map.put("daemonThreads", (long) threadMXBean.getDaemonThreadCount()); + return map; + } + + @Override + public void handleBeforeClassGroup() { + super.handleBeforeClassGroup(); + threadIdsBeforeClassGroup = getAllThreadIds(); + } + + @Override + public void handleAfterClassGroup() { + Set threadIdsBeforeClassGroup = this.threadIdsBeforeClassGroup; + if (threadIdsBeforeClassGroup != null) { + this.threadIdsBeforeClassGroup = null; + + Set threadIds = getAllThreadIds(); + threadIds.removeAll(threadIdsBeforeClassGroup); + + if (!threadIds.isEmpty() && leakageOut != null) { + leakageOut.println("Classes " + testClasses); + leakageOut.println("New threads number: " + threadIds.size()); + + ThreadInfo[] originalThreadInfos = threadMXBean.getThreadInfo(threadIds.stream() + .mapToLong(Long::longValue).toArray(), 64); + leakageOut.println("New threads:"); + + ThreadInfo[] secondTryThreadInfos = originalThreadInfos; + if (CHECK_THREADS_TWICE) { + try { + Thread.sleep(2000L); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + // after sleeping some threads may be shut down (null) - + // monitor them separately and compare with originalThreadInfos + secondTryThreadInfos = threadMXBean.getThreadInfo(threadIds.stream() + .mapToLong(Long::longValue).toArray(), 64); + } + + for (int i = 0; i < secondTryThreadInfos.length; i++) { + ThreadInfo threadInfo = secondTryThreadInfos[i]; + if (threadInfo == null) { + threadInfo = originalThreadInfos[i]; + if (threadInfo == null) { + // there can be a race between listing thread ids and getting thread info + leakageOut.println("---"); + leakageOut.println("Thread is not found (probably already stopped), skipping"); + } else { + leakageOut.println("---"); + leakageOut.println("Thread " + threadInfo.getThreadId() + + " " + threadInfo.getThreadName() + " was shutdown without joining"); + } + } else { + leakageOut.println("---"); + leakageOut.println(threadInfo.getThreadId() + + " (" + threadInfo.getThreadState() + ")" + + ": \"" + threadInfo.getThreadName() + "\""); + for (StackTraceElement stackTraceElement : threadInfo.getStackTrace()) { + leakageOut.println(stackTraceElement); + } + } + } + + leakageOut.println(); + leakageOut.println("--"); + leakageOut.println(); + leakageOut.println(); + } + } else { + logger.warn("threadIdsBeforeClassGroup not initialized"); + } + super.handleAfterClassGroup(); + } + + @Override + public void close() { + leakageOut.close(); + } + + private Set getAllThreadIds() { + Set threadIds = new LinkedHashSet<>(); + for (long threadId : threadMXBean.getAllThreadIds()) { + threadIds.add(threadId); + } + return threadIds; + } +}