From 5b0fc6a8cc59cd44f02a1be2e11cae1e45be9e01 Mon Sep 17 00:00:00 2001 From: Andriy Tsarenko Date: Fri, 8 Feb 2019 04:14:51 +0200 Subject: [PATCH] [#10] add reporter resolvers --- pom.xml | 1 + sprimber-reporter/pom.xml | 63 +++++++++ .../TransformationClassLoaderInitializer.java | 46 +++++++ .../TransformationClassLoader.java | 30 ++++ .../ReportByteCodeResolverException.java | 9 ++ ...mationClassLoaderInitializerException.java | 9 ++ .../holder/ReportDescriptionHolder.java | 34 +++++ .../ReportDescriptionServiceHolder.java | 25 ++++ .../sprimber/reporter/model/MethodInfo.java | 15 ++ .../reporter/model/TestStatusType.java | 8 ++ .../CommonReportByteCodeResolver.java | 129 ++++++++++++++++++ .../resolver/ReportByteCodeResolver.java | 13 ++ .../AssertJReportByteCodeResolver.java | 102 ++++++++++++++ .../service/ReportDescriptionService.java | 28 ++++ .../DevConsoleReportDescriptionResolver.java | 51 +++++++ .../template/ReportMethodTemplates.java | 46 +++++++ .../sprimber/reporter/utils/ClassUtils.java | 43 ++++++ .../main/resources/META-INF/spring.factories | 10 ++ sprimber-spring-boot-starter/pom.xml | 6 +- 19 files changed, 667 insertions(+), 1 deletion(-) create mode 100644 sprimber-reporter/pom.xml create mode 100644 sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/TransformationClassLoaderInitializer.java create mode 100644 sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/classloader/TransformationClassLoader.java create mode 100644 sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/exception/ReportByteCodeResolverException.java create mode 100644 sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/exception/TransformationClassLoaderInitializerException.java create mode 100644 sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/holder/ReportDescriptionHolder.java create mode 100644 sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/holder/ReportDescriptionServiceHolder.java create mode 100644 sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/model/MethodInfo.java create mode 100644 sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/model/TestStatusType.java create mode 100644 sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/resolver/CommonReportByteCodeResolver.java create mode 100644 sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/resolver/ReportByteCodeResolver.java create mode 100644 sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/resolver/assertj/AssertJReportByteCodeResolver.java create mode 100644 sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/service/ReportDescriptionService.java create mode 100644 sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/service/dev/DevConsoleReportDescriptionResolver.java create mode 100644 sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/template/ReportMethodTemplates.java create mode 100644 sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/utils/ClassUtils.java create mode 100644 sprimber-reporter/src/main/resources/META-INF/spring.factories diff --git a/pom.xml b/pom.xml index 3bbac06..0f68e17 100644 --- a/pom.xml +++ b/pom.xml @@ -42,6 +42,7 @@ sprimber-engine + sprimber-reporter sprimber-spring-boot-autoconfigure sprimber-spring-boot-starter sprimber-test diff --git a/sprimber-reporter/pom.xml b/sprimber-reporter/pom.xml new file mode 100644 index 0000000..41d3dc6 --- /dev/null +++ b/sprimber-reporter/pom.xml @@ -0,0 +1,63 @@ + + + + sprimber-parent + com.griddynamics.qa + 1.0.5-SNAPSHOT + + 4.0.0 + + sprimber-reporter + + + + org.javassist + javassist + 3.21.0-GA + + + com.google.guava + guava + 26.0-jre + + + org.apache.commons + commons-lang3 + 3.8.1 + + + org.springframework.boot + spring-boot-starter + 2.0.2.RELEASE + true + + + org.assertj + assertj-core + 3.10.0 + true + + + org.projectlombok + lombok + 1.18.2 + provided + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + org.apache.maven.plugins + maven-source-plugin + + + + + diff --git a/sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/TransformationClassLoaderInitializer.java b/sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/TransformationClassLoaderInitializer.java new file mode 100644 index 0000000..aac5d44 --- /dev/null +++ b/sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/TransformationClassLoaderInitializer.java @@ -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 { + + 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)); + } + } + } + +} diff --git a/sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/classloader/TransformationClassLoader.java b/sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/classloader/TransformationClassLoader.java new file mode 100644 index 0000000..fe529fb --- /dev/null +++ b/sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/classloader/TransformationClassLoader.java @@ -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 reportByteCodeResolvers; + + public TransformationClassLoader(ClassLoader originClassLoader, + List 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)); + } + +} diff --git a/sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/exception/ReportByteCodeResolverException.java b/sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/exception/ReportByteCodeResolverException.java new file mode 100644 index 0000000..fcaa70f --- /dev/null +++ b/sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/exception/ReportByteCodeResolverException.java @@ -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); + } + +} diff --git a/sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/exception/TransformationClassLoaderInitializerException.java b/sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/exception/TransformationClassLoaderInitializerException.java new file mode 100644 index 0000000..4db2b62 --- /dev/null +++ b/sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/exception/TransformationClassLoaderInitializerException.java @@ -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); + } + +} diff --git a/sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/holder/ReportDescriptionHolder.java b/sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/holder/ReportDescriptionHolder.java new file mode 100644 index 0000000..2e61d81 --- /dev/null +++ b/sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/holder/ReportDescriptionHolder.java @@ -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> 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> getReportsByServiceClass() { + return REPORTS_BY_SERVICE_CLASS; + } + + public static List getReports() { + return REPORTS_BY_SERVICE_CLASS.values().stream() + .flatMap(Collection::stream) + .collect(toList()); + } + +} diff --git a/sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/holder/ReportDescriptionServiceHolder.java b/sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/holder/ReportDescriptionServiceHolder.java new file mode 100644 index 0000000..4138db8 --- /dev/null +++ b/sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/holder/ReportDescriptionServiceHolder.java @@ -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 reportDescriptionServices; + + public static void loadReportDescriptionServices(ClassLoader originClassLoader) { + reportDescriptionServices = loadFactories(ReportDescriptionService.class, originClassLoader); + } + + public static List findReportDescriptionServices(String className) { + return reportDescriptionServices.stream() + .filter(service -> service.getSupportedClassNames().contains(className)) + .collect(toList()); + } + +} diff --git a/sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/model/MethodInfo.java b/sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/model/MethodInfo.java new file mode 100644 index 0000000..283e160 --- /dev/null +++ b/sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/model/MethodInfo.java @@ -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; + +} diff --git a/sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/model/TestStatusType.java b/sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/model/TestStatusType.java new file mode 100644 index 0000000..2c3296c --- /dev/null +++ b/sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/model/TestStatusType.java @@ -0,0 +1,8 @@ +package com.griddynamics.qa.sprimber.reporter.model; + +public enum TestStatusType { + + SUCCESS, + FAILED + +} diff --git a/sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/resolver/CommonReportByteCodeResolver.java b/sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/resolver/CommonReportByteCodeResolver.java new file mode 100644 index 0000000..69790b6 --- /dev/null +++ b/sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/resolver/CommonReportByteCodeResolver.java @@ -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> 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> 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> 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; + } + +} diff --git a/sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/resolver/ReportByteCodeResolver.java b/sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/resolver/ReportByteCodeResolver.java new file mode 100644 index 0000000..69e9733 --- /dev/null +++ b/sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/resolver/ReportByteCodeResolver.java @@ -0,0 +1,13 @@ +package com.griddynamics.qa.sprimber.reporter.resolver; + +import java.util.List; + +public interface ReportByteCodeResolver { + + List getSupportedClassNames(); + + boolean isSupportedClass(String className); + + Class insertReport(String className, ClassLoader transformationClassLoader, ClassLoader originClassLoader); + +} diff --git a/sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/resolver/assertj/AssertJReportByteCodeResolver.java b/sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/resolver/assertj/AssertJReportByteCodeResolver.java new file mode 100644 index 0000000..2614f9d --- /dev/null +++ b/sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/resolver/assertj/AssertJReportByteCodeResolver.java @@ -0,0 +1,102 @@ +package com.griddynamics.qa.sprimber.reporter.resolver.assertj; + +import com.google.common.collect.ImmutableList; +import com.griddynamics.qa.sprimber.reporter.exception.ReportByteCodeResolverException; +import com.griddynamics.qa.sprimber.reporter.model.MethodInfo; +import com.griddynamics.qa.sprimber.reporter.resolver.CommonReportByteCodeResolver; +import lombok.SneakyThrows; +import lombok.val; +import lombok.var; +import org.assertj.core.api.AssertionInfo; + +import java.util.List; + +import static javassist.bytecode.AccessFlag.isPublic; +import static org.apache.commons.lang3.exception.ExceptionUtils.rethrow; + +public class AssertJReportByteCodeResolver extends CommonReportByteCodeResolver { + + public static final List ASSERTJ_SUPPORTED_CLASS_NAMES = ImmutableList.of( + // should be processed first, link sequence are required + "org.assertj.core.internal.Comparables", + + "org.assertj.core.internal.Arrays", + "org.assertj.core.internal.BooleanArrays", + "org.assertj.core.internal.Booleans", + "org.assertj.core.internal.ByteArrays", + "org.assertj.core.internal.Characters", + "org.assertj.core.internal.CharArrays", + "org.assertj.core.internal.Classes", + "org.assertj.core.internal.CommonValidations", + "org.assertj.core.internal.Conditions", + "org.assertj.core.internal.Dates", + "org.assertj.core.internal.DoubleArrays", + "org.assertj.core.internal.Files", + "org.assertj.core.internal.FloatArrays", + "org.assertj.core.internal.Futures", + "org.assertj.core.internal.InputStreams", + "org.assertj.core.internal.IntArrays", + "org.assertj.core.internal.Iterables", + "org.assertj.core.internal.Lists", + "org.assertj.core.internal.LongArrays", + "org.assertj.core.internal.Maps", + "org.assertj.core.internal.Numbers", + "org.assertj.core.internal.ObjectArrays", + "org.assertj.core.internal.Objects", + "org.assertj.core.internal.Paths", + "org.assertj.core.internal.RealNumbers", + "org.assertj.core.internal.ShortArrays", + "org.assertj.core.internal.Strings", + "org.assertj.core.internal.Throwables", + "org.assertj.core.internal.Uris", + "org.assertj.core.internal.Urls" + ); + + @Override + public List getSupportedClassNames() { + return ASSERTJ_SUPPORTED_CLASS_NAMES; + } + + @Override + public boolean isSupportedClass(String className) { + return ASSERTJ_SUPPORTED_CLASS_NAMES.contains(className); + } + + @Override + public Class insertReport(String className, + ClassLoader transformationClassLoader, ClassLoader originClassLoader) { + try { + return acceptMethodsTransformation(className + , transformationClassLoader, originClassLoader, methodsInfoStream -> + methodsInfoStream.forEach(this::injectReportByClassOfMethod)); + } catch (Throwable e) { + return rethrow(new ReportByteCodeResolverException( + "Unable to inject 'report', target class: " + className, e)); + } + } + + @SneakyThrows + protected int findAssertionDescriptionParamIndex(MethodInfo methodInfo) { + val ctMethod = methodInfo.getCtMethod(); + var assertionDescriptionParamIndex = -1; + if (isPublic(ctMethod.getModifiers())) { + for (int i = 0; i < ctMethod.getParameterTypes().length; i++) { + val paramType = ctMethod.getParameterTypes()[i]; + if (AssertionInfo.class.getName().equals(paramType.getName())) { + assertionDescriptionParamIndex = i + 1; + break; + } + } + } + return assertionDescriptionParamIndex; + } + + private void injectReportByClassOfMethod(MethodInfo methodInfo) { + val ctClass = methodInfo.getCtClass(); + val ctMethod = methodInfo.getCtMethod(); + if (!isAlreadyTransformedMethod(ctMethod) && ASSERTJ_SUPPORTED_CLASS_NAMES.contains(ctClass.getName())) { + injectReportToMethod(methodInfo); + } + } + +} diff --git a/sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/service/ReportDescriptionService.java b/sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/service/ReportDescriptionService.java new file mode 100644 index 0000000..6c2777e --- /dev/null +++ b/sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/service/ReportDescriptionService.java @@ -0,0 +1,28 @@ +package com.griddynamics.qa.sprimber.reporter.service; + +import lombok.Synchronized; +import lombok.val; + +import java.util.List; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; + +@SuppressWarnings({"unused", "WeakerAccess"}) +public abstract class ReportDescriptionService { + + protected static final String REPORT_DESCRIPTION_MASK = "Assert that: %s, status: %s"; + protected Set processedDescriptionInfos = new CopyOnWriteArraySet<>(); + + public abstract List getSupportedClassNames(); + + public abstract void sendReportDescription(Object descriptionInfo, String description, String status); + + @Synchronized + protected boolean alreadyProcessedDescriptionInfo(Object descriptionInfo) { + val processed = processedDescriptionInfos.stream() + .anyMatch(info -> info == descriptionInfo); + processedDescriptionInfos.add(descriptionInfo); + return processed; + } + +} diff --git a/sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/service/dev/DevConsoleReportDescriptionResolver.java b/sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/service/dev/DevConsoleReportDescriptionResolver.java new file mode 100644 index 0000000..e7870d7 --- /dev/null +++ b/sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/service/dev/DevConsoleReportDescriptionResolver.java @@ -0,0 +1,51 @@ +package com.griddynamics.qa.sprimber.reporter.service.dev; + +import com.griddynamics.qa.sprimber.reporter.service.ReportDescriptionService; +import lombok.val; + +import java.util.List; + +import static com.griddynamics.qa.sprimber.reporter.holder.ReportDescriptionHolder.getReportsByServiceClass; +import static com.griddynamics.qa.sprimber.reporter.holder.ReportDescriptionHolder.putReport; +import static com.griddynamics.qa.sprimber.reporter.resolver.assertj.AssertJReportByteCodeResolver.ASSERTJ_SUPPORTED_CLASS_NAMES; +import static java.lang.Runtime.getRuntime; +import static java.lang.String.format; +import static java.lang.System.getProperty; +import static org.springframework.util.StringUtils.commaDelimitedListToSet; + +public class DevConsoleReportDescriptionResolver extends ReportDescriptionService { + + private static final boolean DEV_ACTIVE = commaDelimitedListToSet(getProperty("spring.profiles.active")) + .contains("sr_dev"); + + public DevConsoleReportDescriptionResolver() { + if (DEV_ACTIVE) { + getRuntime().addShutdownHook(new Thread(this::printAllReports)); + } + } + + @Override + public List getSupportedClassNames() { + return ASSERTJ_SUPPORTED_CLASS_NAMES; + } + + @Override + public void sendReportDescription(Object descriptionInfo, String description, String status) { + if (DEV_ACTIVE) { + if (!alreadyProcessedDescriptionInfo(descriptionInfo)) { + val report = format(REPORT_DESCRIPTION_MASK, description, status); + putReport(getClass(), report); + System.out.println(report); + } + } + } + + private void printAllReports() { + getReportsByServiceClass().forEach((service, reports) -> { + System.out.println("\nReportDescriptionService: " + service); + System.out.println("Reports:"); + reports.forEach(report -> System.out.print(report + "\n")); + }); + } + +} diff --git a/sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/template/ReportMethodTemplates.java b/sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/template/ReportMethodTemplates.java new file mode 100644 index 0000000..a52fc70 --- /dev/null +++ b/sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/template/ReportMethodTemplates.java @@ -0,0 +1,46 @@ +package com.griddynamics.qa.sprimber.reporter.template; + +import lombok.NoArgsConstructor; + +import static lombok.AccessLevel.PRIVATE; + +@NoArgsConstructor(access = PRIVATE) +public final class ReportMethodTemplates { + + public static final String PROCESS_REPORT_CODE_BLOCK_MASK = + "{" + + "try {" + + + // invoke origin method + "%s" + + + //send report with 'success' status + "%s" + + + "} catch (Throwable t) {" + + + //send report with 'failed' status + "%s" + + "org.apache.commons.lang3.exception.ExceptionUtils.rethrow(t);" + + "}" + + "}"; + + public static final String ASSERTJ_SEND_REPORT_CODE_BLOCK_MASK = + // List of ReportDescriptionService's + "java.util.List reportDescriptionServices = " + + // find by target className + "com.griddynamics.qa.sprimber.reporter.holder.ReportDescriptionServiceHolder" + + ".findReportDescriptionServices(%s);" + + + "for (int i = 0; i < reportDescriptionServices.size(); i++) {" + + + "com.griddynamics.qa.sprimber.reporter.service.ReportDescriptionService service = " + + // get target ReportDescriptionService + "(com.griddynamics.qa.sprimber.reporter.service.ReportDescriptionService)" + + "reportDescriptionServices.get(i);" + + + // send description + "service.sendReportDescription($%s, java.lang.String.valueOf($%s.description()), %s);" + + "}"; + +} diff --git a/sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/utils/ClassUtils.java b/sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/utils/ClassUtils.java new file mode 100644 index 0000000..e6151a8 --- /dev/null +++ b/sprimber-reporter/src/main/java/com/griddynamics/qa/sprimber/reporter/utils/ClassUtils.java @@ -0,0 +1,43 @@ +package com.griddynamics.qa.sprimber.reporter.utils; + +import javassist.CtClass; +import lombok.*; + +import java.io.DataInputStream; + +import static java.util.Objects.requireNonNull; +import static lombok.AccessLevel.PRIVATE; + +@NoArgsConstructor(access = PRIVATE) +public final class ClassUtils { + + private static final char CLASS_NAME_SEPARATOR = '.'; + private static final char CLASS_PATH_SEPARATOR = '/'; + private static final String CLASS_SUFFIX = ".class"; + + @SneakyThrows + public static byte[] classToBytes(String className, ClassLoader classLoader) { + val classAsPath = className.replace(CLASS_NAME_SEPARATOR, CLASS_PATH_SEPARATOR) + CLASS_SUFFIX; + val inputStream = classLoader.getResourceAsStream(classAsPath); + requireNonNull(inputStream, "Class path not found: " + className); + @Cleanup val dataInputStream = new DataInputStream(inputStream); + val classBytes = new byte[inputStream.available()]; + dataInputStream.readFully(classBytes); + return classBytes; + } + + @SneakyThrows + public static void deFrostClass(CtClass ctClass) { + if (ctClass.isFrozen()) { + ctClass.defrost(); + } + var parent = ctClass.getSuperclass(); + while (parent != null) { + if (!parent.getName().equals(Object.class.getName()) && parent.isFrozen()) { + parent.defrost(); + } + parent = parent.getSuperclass(); + } + } + +} diff --git a/sprimber-reporter/src/main/resources/META-INF/spring.factories b/sprimber-reporter/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..f7066cf --- /dev/null +++ b/sprimber-reporter/src/main/resources/META-INF/spring.factories @@ -0,0 +1,10 @@ +org.springframework.context.ApplicationContextInitializer=\ + com.griddynamics.qa.sprimber.reporter.TransformationClassLoaderInitializer + +# API, AssertJ implementation +com.griddynamics.qa.sprimber.reporter.resolver.ReportByteCodeResolver=\ + com.griddynamics.qa.sprimber.reporter.resolver.assertj.AssertJReportByteCodeResolver + +# API, dev mode (if `sr_dev` profile is active) +com.griddynamics.qa.sprimber.reporter.service.ReportDescriptionService=\ + com.griddynamics.qa.sprimber.reporter.service.dev.DevConsoleReportDescriptionResolver diff --git a/sprimber-spring-boot-starter/pom.xml b/sprimber-spring-boot-starter/pom.xml index f2c52f6..05825cf 100644 --- a/sprimber-spring-boot-starter/pom.xml +++ b/sprimber-spring-boot-starter/pom.xml @@ -29,7 +29,11 @@ sprimber-engine 1.0.5-SNAPSHOT - + + com.griddynamics.qa + sprimber-reporter + 1.0.5-SNAPSHOT + org.assertj assertj-core