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
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@

<modules>
<module>sprimber-engine</module>
<module>sprimber-reporter</module>
<module>sprimber-spring-boot-autoconfigure</module>
<module>sprimber-spring-boot-starter</module>
<module>sprimber-test</module>
Expand Down
63 changes: 63 additions & 0 deletions sprimber-reporter/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>sprimber-parent</artifactId>
<groupId>com.griddynamics.qa</groupId>
<version>1.0.5-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>sprimber-reporter</artifactId>

<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.21.0-GA</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>26.0-jre</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.8.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.0.2.RELEASE</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.10.0</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.2</version>
<scope>provided</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.griddynamics.qa.sprimber.reporter;

import com.griddynamics.qa.sprimber.reporter.classloader.TransformationClassLoader;
import com.griddynamics.qa.sprimber.reporter.exception.TransformationClassLoaderInitializerException;
import com.griddynamics.qa.sprimber.reporter.resolver.ReportByteCodeResolver;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.support.GenericApplicationContext;

import javax.annotation.Nonnull;

import java.util.concurrent.atomic.AtomicBoolean;

import static com.griddynamics.qa.sprimber.reporter.holder.ReportDescriptionServiceHolder.loadReportDescriptionServices;
import static org.apache.commons.lang3.exception.ExceptionUtils.rethrow;
import static org.springframework.core.io.support.SpringFactoriesLoader.loadFactories;

@Slf4j
public class TransformationClassLoaderInitializer implements ApplicationContextInitializer<GenericApplicationContext> {

private static final AtomicBoolean INITIALIZE_TRANSFORMATION_CLASS_LOADER = new AtomicBoolean(true);

@Override
public void initialize(@Nonnull GenericApplicationContext context) {
if (INITIALIZE_TRANSFORMATION_CLASS_LOADER.getAndSet(false)) {
try {
val originClassLoader = context.getClassLoader();
loadReportDescriptionServices(originClassLoader);
val reportByteCodeResolvers = loadFactories(ReportByteCodeResolver.class, originClassLoader);
val transformationClassLoader = new TransformationClassLoader(originClassLoader, reportByteCodeResolvers);
context.setClassLoader(transformationClassLoader);
for (ReportByteCodeResolver resolver : reportByteCodeResolvers) {
for (String className : resolver.getSupportedClassNames()) {
val transformedClass = transformationClassLoader.loadClass(className);
log.debug("Class was transformed: " + transformedClass);
}
}
} catch (Throwable e) {
rethrow(new TransformationClassLoaderInitializerException(
"Unable to initialize 'TransformationClassLoader', cause: " + e.getMessage(), e));
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.griddynamics.qa.sprimber.reporter.classloader;

import com.griddynamics.qa.sprimber.reporter.resolver.ReportByteCodeResolver;

import java.net.URL;
import java.net.URLClassLoader;
import java.util.List;

public class TransformationClassLoader extends URLClassLoader {

private final ClassLoader originClassLoader;
private final List<ReportByteCodeResolver> reportByteCodeResolvers;

public TransformationClassLoader(ClassLoader originClassLoader,
List<ReportByteCodeResolver> reportByteCodeResolvers) {
super(new URL[]{}, originClassLoader);
this.originClassLoader = originClassLoader;
this.reportByteCodeResolvers = reportByteCodeResolvers;
}

@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
return reportByteCodeResolvers.stream()
.filter(resolver -> resolver.isSupportedClass(name))
.findFirst()
.map(resolver -> resolver.insertReport(name, this, originClassLoader))
.orElse(super.loadClass(name));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.griddynamics.qa.sprimber.reporter.exception;

public class ReportByteCodeResolverException extends Throwable {

public ReportByteCodeResolverException(String message, Throwable cause) {
super(message, cause);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.griddynamics.qa.sprimber.reporter.exception;

public class TransformationClassLoaderInitializerException extends Throwable {

public TransformationClassLoaderInitializerException(String message, Throwable cause) {
super(message, cause);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.griddynamics.qa.sprimber.reporter.holder;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import static com.google.common.collect.Lists.newArrayList;
import static java.util.stream.Collectors.toList;

@SuppressWarnings("unused")
public class ReportDescriptionHolder {

private static final Map<Class, List<String>> REPORTS_BY_SERVICE_CLASS = new ConcurrentHashMap<>();

public static void putReport(Class<?> serviceClass, String report) {
REPORTS_BY_SERVICE_CLASS.computeIfPresent(serviceClass, (service, reports) -> {
reports.add(report);
return reports;
});
REPORTS_BY_SERVICE_CLASS.putIfAbsent(serviceClass, newArrayList(report));
}

public static Map<Class, List<String>> getReportsByServiceClass() {
return REPORTS_BY_SERVICE_CLASS;
}

public static List<String> getReports() {
return REPORTS_BY_SERVICE_CLASS.values().stream()
.flatMap(Collection::stream)
.collect(toList());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.griddynamics.qa.sprimber.reporter.holder;

import com.griddynamics.qa.sprimber.reporter.service.ReportDescriptionService;

import java.util.List;

import static java.util.stream.Collectors.toList;
import static org.springframework.core.io.support.SpringFactoriesLoader.loadFactories;

@SuppressWarnings("unused")
public class ReportDescriptionServiceHolder {

private static List<ReportDescriptionService> reportDescriptionServices;

public static void loadReportDescriptionServices(ClassLoader originClassLoader) {
reportDescriptionServices = loadFactories(ReportDescriptionService.class, originClassLoader);
}

public static List<ReportDescriptionService> findReportDescriptionServices(String className) {
return reportDescriptionServices.stream()
.filter(service -> service.getSupportedClassNames().contains(className))
.collect(toList());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.griddynamics.qa.sprimber.reporter.model;

import javassist.CtClass;
import javassist.CtMethod;
import lombok.Data;
import lombok.RequiredArgsConstructor;

@Data
@RequiredArgsConstructor
public class MethodInfo {

private final CtClass ctClass;
private final CtMethod ctMethod;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.griddynamics.qa.sprimber.reporter.model;

public enum TestStatusType {

SUCCESS,
FAILED

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package com.griddynamics.qa.sprimber.reporter.resolver;

import com.griddynamics.qa.sprimber.reporter.exception.ReportByteCodeResolverException;
import com.griddynamics.qa.sprimber.reporter.model.MethodInfo;
import com.griddynamics.qa.sprimber.reporter.model.TestStatusType;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.LoaderClassPath;
import lombok.SneakyThrows;
import lombok.val;

import java.io.ByteArrayInputStream;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.stream.Stream;

import static com.griddynamics.qa.sprimber.reporter.model.TestStatusType.FAILED;
import static com.griddynamics.qa.sprimber.reporter.model.TestStatusType.SUCCESS;
import static com.griddynamics.qa.sprimber.reporter.template.ReportMethodTemplates.PROCESS_REPORT_CODE_BLOCK_MASK;
import static com.griddynamics.qa.sprimber.reporter.template.ReportMethodTemplates.ASSERTJ_SEND_REPORT_CODE_BLOCK_MASK;
import static com.griddynamics.qa.sprimber.reporter.utils.ClassUtils.classToBytes;
import static com.griddynamics.qa.sprimber.reporter.utils.ClassUtils.deFrostClass;
import static java.lang.String.format;
import static java.util.Arrays.stream;
import static java.util.UUID.randomUUID;
import static javassist.ClassPool.getDefault;
import static org.apache.commons.lang3.StringUtils.EMPTY;
import static org.apache.commons.lang3.exception.ExceptionUtils.rethrow;

public abstract class CommonReportByteCodeResolver implements ReportByteCodeResolver {

private static final Map<String, Class<?>> CACHED_TRANSFORMED_CLASSES = new ConcurrentHashMap<>();

private static final String TRANSFORMED_METHOD_NAME_PREFIX = "sr$";
private static final String METHOD_PARAM_PREFIX = "$";
private static final String METHDO_PARAM_DELIMITER = ",";
private static final String METHOD_OPEN_BRACKET = "(";
private static final String METHOD_CLOSED_BRACKET = ");";

private static final String WRAPPED_STRING_QUOTE = "\"";
private static final String UUID_DELIMITER = "-";

protected Class<?> acceptMethodsTransformation(String className,
ClassLoader transformationClassLoader, ClassLoader originClassLoader,
Consumer<Stream<MethodInfo>> methodsInfoConsumer) {
if (!CACHED_TRANSFORMED_CLASSES.containsKey(className)) {
synchronized (CommonReportByteCodeResolver.class) {
if (!CACHED_TRANSFORMED_CLASSES.containsKey(className)) {
try {
doAcceptMethodsTransformation(className,
transformationClassLoader, originClassLoader, methodsInfoConsumer);
} catch (Throwable e) {
return rethrow(new ReportByteCodeResolverException(
"Unable to transform methods, class: " + className, e));
}
}
}
}
return CACHED_TRANSFORMED_CLASSES.get(className);
}

protected void injectReportToMethod(MethodInfo methodInfo) {
val assertionDescriptionParamIndex = findAssertionDescriptionParamIndex(methodInfo);
if (assertionDescriptionParamIndex != -1) {
setupReportProcessingToByteCode(methodInfo.getCtClass(),
methodInfo.getCtMethod(), assertionDescriptionParamIndex);
}
}

protected boolean isAlreadyTransformedMethod(CtMethod ctMethod) {
return ctMethod.getName().startsWith(TRANSFORMED_METHOD_NAME_PREFIX);
}

protected abstract int findAssertionDescriptionParamIndex(MethodInfo methodInfo);

@SneakyThrows
private void doAcceptMethodsTransformation(String className,
ClassLoader transformationClassLoader, ClassLoader originClassLoader,
Consumer<Stream<MethodInfo>> methodsInfoConsumer) {
val classPool = getDefault();
classPool.appendClassPath(new LoaderClassPath(transformationClassLoader));
val ctClass = classPool.makeClass(new ByteArrayInputStream(classToBytes(className, originClassLoader)));
val methodsInfoStream = stream(ctClass.getMethods())
.map(ctMethod -> new MethodInfo(ctClass, ctMethod));
methodsInfoConsumer.accept(methodsInfoStream);
CACHED_TRANSFORMED_CLASSES.put(className, ctClass.toClass());
}

@SneakyThrows
private void setupReportProcessingToByteCode(CtClass ctClass, CtMethod ctMethod,
int assertionDescriptionParamIndex) {
val transformedCtMethod = new CtMethod(ctMethod, ctClass, null);
deFrostClass(ctClass);
ctMethod.setName(generateMethodName());
val sendSuccessReportCodeBlock = buildSendReportCodeBlock(ctClass, assertionDescriptionParamIndex, SUCCESS);
val sendFailedReportCodeBlock = buildSendReportCodeBlock(ctClass, assertionDescriptionParamIndex, FAILED);
val transformedMethodCodeBlock = format(PROCESS_REPORT_CODE_BLOCK_MASK,
buildInvokeOriginMethodCodeBlock(ctMethod), sendSuccessReportCodeBlock, sendFailedReportCodeBlock);
transformedCtMethod.setBody(transformedMethodCodeBlock);
ctClass.addMethod(transformedCtMethod);
}

private String generateMethodName() {
return TRANSFORMED_METHOD_NAME_PREFIX + randomUUID().toString()
.replaceAll(UUID_DELIMITER, EMPTY);
}

private String buildSendReportCodeBlock(CtClass ctClass, int assertionDescriptionParamIndex,
TestStatusType testStatusType) {
val status = WRAPPED_STRING_QUOTE + testStatusType + WRAPPED_STRING_QUOTE;
val className = WRAPPED_STRING_QUOTE + ctClass.getName() + WRAPPED_STRING_QUOTE;
return format(ASSERTJ_SEND_REPORT_CODE_BLOCK_MASK, className,
assertionDescriptionParamIndex, assertionDescriptionParamIndex, status);
}

@SneakyThrows
private String buildInvokeOriginMethodCodeBlock(CtMethod ctMethod) {
val methodParametersCodeBlock = new StringBuilder();
for (int i = 0; i < ctMethod.getParameterTypes().length; i++) {
methodParametersCodeBlock.append(METHOD_PARAM_PREFIX).append(i + 1);
if ((i + 1) < ctMethod.getParameterTypes().length) {
methodParametersCodeBlock.append(METHDO_PARAM_DELIMITER);
}
}
return ctMethod.getName() + METHOD_OPEN_BRACKET + methodParametersCodeBlock.toString() + METHOD_CLOSED_BRACKET;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.griddynamics.qa.sprimber.reporter.resolver;

import java.util.List;

public interface ReportByteCodeResolver {

List<String> getSupportedClassNames();

boolean isSupportedClass(String className);

Class insertReport(String className, ClassLoader transformationClassLoader, ClassLoader originClassLoader);

}
Loading