Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, Long> getIndicators() {
Map<String, Long> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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<String> headers;

public ResourceLeakageCsvLogWriter(File outputFile, List<String> 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<String, Long> 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();
}
}
Original file line number Diff line number Diff line change
@@ -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<String> indicatorKeys;

List<Class<?>> testClasses;

protected ResourceLeakageDetector(List<String> indicatorKeys) {
this.indicatorKeys = Collections.unmodifiableList(new ArrayList<>(indicatorKeys));
}

public final List<String> getIndicatorKeys() {
return indicatorKeys;
}

public abstract Map<String, Long> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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<String, Long> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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<ResourceLeakageDetector> 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<ResourceLeakageDetector> resourceLeakageDetectors) {
/*@Nullable*/
File reportsBaseDir = ResourceLeakageUtils.getReportsBaseDir();

this.resourceLeakageDetectors = resourceLeakageDetectors;
if (reportsBaseDir == null) {
this.resourceLeakageLogWriter = null;
} else {
File outputFile = new File(reportsBaseDir, "report.csv");
List<String> 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<String, Long> indicators = new HashMap<>();
resourceLeakageDetectors.forEach(detector -> indicators.putAll(detector.getIndicators()));
resourceLeakageLogWriter.write(indicators, testClass, event,
testGroupNumber.get(), testNumber.get());
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading