Skip to content

Commit

Permalink
[#10] add reporter resolvers
Browse files Browse the repository at this point in the history
  • Loading branch information
Andriy Tsarenko committed Apr 1, 2019
1 parent d928493 commit 5b0fc6a
Show file tree
Hide file tree
Showing 19 changed files with 667 additions and 1 deletion.
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

0 comments on commit 5b0fc6a

Please sign in to comment.