Skip to content

Commit

Permalink
scalatest FuncSuite support (#12)
Browse files Browse the repository at this point in the history
This patch adds support for FuncSuite with the following changes:

- New agent to define for ignoring tests because when the runner loads
the test ignorer, the class has been already loaded, which means that
can not be changed.

- Checking if the test extends from FuncSuite

- ScalaTest creates a constructor per Test class, and inside the constructor, calls the tests.
This patch ignores this kind of tests by replacing the method call "test" by "ignore".
  • Loading branch information
rpau committed Aug 16, 2018
1 parent 684679d commit 969266e
Show file tree
Hide file tree
Showing 8 changed files with 332 additions and 35 deletions.
17 changes: 17 additions & 0 deletions junit4git/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,23 @@ dependencies {
testCompile group: 'org.mockito', name: 'mockito-all', version: '1.10.19'
}

Map <String, ?> attrs = [ 'Premain-Class': 'org.walkmod.junit4git.core.ignorers.TestIgnorerAgent',
'Can-Retransform-Classes': true,
'Boot-Class-Path': 'junit4git-agent.jar'
]

task customFatJar(type: Jar) {
manifest {
attributes attrs
}
baseName = 'junit4git-agent'
from {
configurations.compile.collect { it.isDirectory() ? it : zipTree(it)}
}
exclude ('**/*.RSA')
with jar
}

publishing {
publications {
mavenJava(MavenPublication) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package org.walkmod.junit4git.core.bytecode;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.walkmod.junit4git.core.ignorers.TestIgnorer;
import org.walkmod.junit4git.core.reports.TestMethodReport;
import org.walkmod.junit4git.javassist.JavassistUtils;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import java.util.*;


public class TestIgnorerTransformer implements ClassFileTransformer {


private static Log log = LogFactory.getLog(TestIgnorerTransformer.class);

private final TestIgnorer testIgnorer;

private final Map<String, List<TestMethodReport>> testsToMap;

public TestIgnorerTransformer(TestIgnorer testIgnorer) throws Exception {
this.testIgnorer = testIgnorer;
testsToMap = testIgnorer.testsGroupedByClass();
log.info("Last Test Impact Analysis: " + testsToMap.size() + " tests");
}

@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer)
throws IllegalClassFormatException {
String name = normalizeName(className);
try {
if (testsToMap.containsKey(name)) {
log.info("Ignoring " + name);
return testIgnorer.ignoreTest(name, testsToMap.get(name));
}
return classfileBuffer;

} catch (Exception e) {
log.error("Error ignoring the tests of " + name, e);
throw new IllegalClassFormatException("Error ignoring tests on " + name);
}
}

private String normalizeName(String className) {
String aux = className.replaceAll("/", "\\.");
if (aux.endsWith(".class")) {
aux = aux.substring(0, aux.length() - ".class".length());
}
return aux;
}

}
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package org.walkmod.junit4git.core.ignorers;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.NotFoundException;
import javassist.bytecode.ClassFile;
import javassist.bytecode.ConstPool;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.junit.Ignore;
Expand All @@ -12,12 +17,10 @@

import java.io.File;
import java.io.IOException;
import java.lang.instrument.ClassDefinition;
import java.lang.instrument.Instrumentation;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

Expand All @@ -33,6 +36,8 @@ public class TestIgnorer {

private final JavassistUtils javassist;

private static Log log = LogFactory.getLog(TestIgnorer.class);

public TestIgnorer(AbstractTestReportStorage storage) {
this(".", storage);
}
Expand Down Expand Up @@ -72,7 +77,7 @@ private Set<TestMethodReport> testsToIgnore(Set<String> status, TestMethodReport
protected Git open() throws IOException, GitAPIException {
File file = executionDir();
boolean isGit = new File(file, ".git").exists();
while(!isGit && file != null) {
while (!isGit && file != null) {
file = file.getParentFile();
isGit = new File(file, ".git").exists();
}
Expand Down Expand Up @@ -114,7 +119,7 @@ protected Set<String> getFilesWithUntrackedChanges() throws IOException, GitAPIE
}
}

private Map<String, List<TestMethodReport>> testsGroupedByClass() throws Exception {
public Map<String, List<TestMethodReport>> testsGroupedByClass() throws Exception {
return getTestsToIgnore(storage.getBaseReport()).stream()
.collect(Collectors.groupingBy(TestMethodReport::getTestClass));
}
Expand All @@ -126,6 +131,73 @@ private Map<String, List<TestMethodReport>> testsGroupedByClass() throws Excepti
* @throws Exception in case of modification issues.
*/
public void ignoreTests(Instrumentation inst) throws Exception {
javassist.annotateMethods(Ignore.class, inst, testsGroupedByClass());
ClassPool pool = ClassPool.getDefault();
ignoreTests(inst, pool);
}

public void ignoreTests(Instrumentation inst, ClassPool pool) throws Exception {
Map<String, List<TestMethodReport>> testsToMap = testsGroupedByClass();

Iterator<String> it = testsToMap.keySet().iterator();
while (it.hasNext()) {
String className = it.next();
ignoreTest(testsToMap, className, Class.forName(className), inst, pool);
}
}

public void ignoreTest(Map<String, List<TestMethodReport>> testsToMap, String className, Class<?> loadedClass,
Instrumentation inst, ClassPool pool) {
try {
CtClass clazz = pool.get(className);
ClassDefinition classDefinition = new ClassDefinition(loadedClass,
ignoreTest(clazz, testsToMap.get(className)));
inst.redefineClasses(classDefinition);
log.info("The test class " + className + " will be ignored");
} catch (NotFoundException | ClassNotFoundException e) {
//the class has been removed
} catch (UnsupportedOperationException e) {
log.error("Error reloading the class because it was initially loaded");
} catch (Throwable e) {
log.error("Error ignoring the test class " + className, e);
}
}

public byte[] ignoreTest(CtClass clazz, List<TestMethodReport> methods) throws Exception {
String className = clazz.getName();
clazz.defrost();
ClassFile ccFile = clazz.getClassFile();
if (isScalaTest(clazz)) {
//TODO: Filter by test method (ie. constructor)
javassist.replaceMethodCallOnConstructors("test", "ignore", clazz);
log.debug("The scala test class " + className + " has been processed");
} else {
ConstPool constpool = ccFile.getConstPool();
methods.stream()
.map(TestMethodReport::getTestMethod)
.forEach(md -> javassist.annotateMethod(Ignore.class, md, clazz, constpool));
}
log.debug("The test class " + className + " will be ignored");
return clazz.toBytecode();
}

public byte[] ignoreTest(String className, List<TestMethodReport> methods) throws Exception {

ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.get(className);
return ignoreTest(clazz, methods);
}

public boolean isScalaTest(CtClass clazz) {
try {
CtClass superClazz = clazz.getSuperclass();
while (superClazz != null
&& !superClazz.getName().equals("org.scalatest.FunSuite")
&& !superClazz.getName().equals("java.lang.Object")) {
superClazz = superClazz.getSuperclass();
}
return superClazz != null && superClazz.getName().equals("org.scalatest.FunSuite");
} catch (Exception e) {
return false;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.walkmod.junit4git.core.ignorers;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.walkmod.junit4git.core.bytecode.TestIgnorerTransformer;
import org.walkmod.junit4git.core.reports.GitTestReportStorage;

import java.lang.instrument.Instrumentation;

public class TestIgnorerAgent {

private static Log log = LogFactory.getLog(TestIgnorerAgent.class);

public static void premain(String args, Instrumentation instrumentation) {
log.info("JUnit4Git agent started");
try {
instrumentation.addTransformer(new TestIgnorerTransformer(new TestIgnorer(new GitTestReportStorage())));
} catch (Exception e) {
log.error(e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,22 @@
import javassist.bytecode.ConstPool;
import javassist.bytecode.MethodInfo;
import javassist.bytecode.annotation.Annotation;
import javassist.expr.ExprEditor;
import javassist.expr.MethodCall;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.walkmod.junit4git.core.reports.TestMethodReport;

import java.io.IOException;
import java.lang.instrument.ClassDefinition;
import java.lang.instrument.Instrumentation;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.*;

public class JavassistUtils {

private static Log log = LogFactory.getLog(JavassistUtils.class);

public void annotateMethod(Class<?> annotationClass,
public boolean annotateMethod(Class<?> annotationClass,
String md, CtClass clazz, ConstPool constpool) {
try {
CtMethod method = clazz.getDeclaredMethod(md);
Expand All @@ -35,13 +34,42 @@ public void annotateMethod(Class<?> annotationClass,

methodAttr.addAnnotation(new Annotation(annotationClass.getName(), constpool));
method.getMethodInfo().addAttribute(methodAttr);
return true;
} catch (NotFoundException e) {
//the method has been removed
log.error("The method " + md + "does not exists in " + clazz.getName());
return false;
} catch (Exception e) {
throw new RuntimeException("Error adding @" + annotationClass.getName() + " annotations", e);
}
}

public void replaceMethodCallOnConstructors(String fromMethodName, String toMethodName, CtClass clazz) {

CtConstructor[] constructors = clazz.getConstructors();
for (CtConstructor constructor: constructors) {
try {
CodeConverter codeConverter = new CodeConverter();

CtMethod oldMethod = Arrays.stream(clazz.getMethods())
.filter(method -> method.getName().equals(fromMethodName))
.findFirst().get();

CtMethod newMethod = Arrays.stream(clazz.getMethods())
.filter(method -> method.getName().equals(toMethodName))
.findFirst().get();

codeConverter.redirectMethodCall(oldMethod, newMethod);

constructor.instrument(codeConverter);

} catch (CannotCompileException e) {
log.error("The constructor of " + clazz.getName() + " cannot be adapted to ignore tests", e);
}
}

}

public void annotateMethods(Class<?> annotationClass, Instrumentation inst, Map<String,
List<TestMethodReport>> testsToMap) {
annotateMethods(annotationClass, inst, ClassPool.getDefault(), testsToMap);
Expand All @@ -60,11 +88,10 @@ public void annotateMethods(Class<?> annotationClass, Instrumentation inst,
testsToMap.get(className).stream()
.map(TestMethodReport::getTestMethod)
.forEach(md -> annotateMethod(annotationClass, md, clazz, constpool));
if (clazz.isFrozen()) {
clazz.defrost();
clazz.detach();
}

clazz.defrost();
inst.redefineClasses(new ClassDefinition(Class.forName(className), clazz.toBytecode()));
log.info("The test class " + className + " will be ignored");
} catch (NotFoundException | ClassNotFoundException e) {
//the class has been removed
} catch (Throwable e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package org.walkmod.junit4git.core.ignorers;

import com.google.gson.Gson;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.bytecode.ClassFile;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.junit.Assert;
import org.junit.Ignore;
Expand Down Expand Up @@ -218,28 +221,38 @@ public void resolves_the_tests_to_ignore() throws Exception {
.build();
GitRepo clonedRepo = GitRepoBuilder.clone(repo).build();

//preparing mocks
JavassistUtils javassist = mock(JavassistUtils.class);
try {
//preparing mocks
JavassistUtils javassist = mock(JavassistUtils.class);
ClassPool pool = mock(ClassPool.class);

AbstractTestReportStorage storage = mock(AbstractTestReportStorage.class);
TestMethodReport methodReport = new TestMethodReport("Test", "foo", Collections.EMPTY_SET);
when(storage.getBaseReport()).thenReturn(new TestMethodReport[]{methodReport});
CtClass clazz = mock(CtClass.class);

//preparing test ignorer
TestIgnorer ignorer = new TestIgnorer(clonedRepo.getPath().toFile().getCanonicalPath(),
storage, javassist);
ClassFile classFile = new ClassFile(false, "java.lang.Object", null);
when(clazz.getClassFile()).thenReturn(classFile);

Instrumentation inst = mock(Instrumentation.class);
ignorer.ignoreTests(inst);
when(pool.get("Test")).thenReturn(clazz);

//validating results
Map<String, List<TestMethodReport>> testsToIgnore = new HashMap<>();
testsToIgnore.put("Test", Arrays.asList(methodReport));
verify(javassist).annotateMethods(Ignore.class, inst, testsToIgnore);
AbstractTestReportStorage storage = mock(AbstractTestReportStorage.class);
TestMethodReport methodReport = new TestMethodReport("Test", "foo", Collections.EMPTY_SET);
when(storage.getBaseReport()).thenReturn(new TestMethodReport[]{methodReport});

//deleting repos
repo.delete();
clonedRepo.delete();
//preparing test ignorer
TestIgnorer ignorer = new TestIgnorer(clonedRepo.getPath().toFile().getCanonicalPath(),
storage, javassist);

Instrumentation inst = mock(Instrumentation.class);
ignorer.ignoreTest(ignorer.testsGroupedByClass(), "Test", null, inst, pool);

//validating results
Map<String, List<TestMethodReport>> testsToIgnore = new HashMap<>();
testsToIgnore.put("Test", Arrays.asList(methodReport));
verify(javassist).annotateMethod(Ignore.class, "foo", clazz, classFile.getConstPool());
} finally {
//deleting repos
repo.delete();
clonedRepo.delete();
}
}

}
Loading

0 comments on commit 969266e

Please sign in to comment.