/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.kie.workbench.common.services.refactoring.backend.server.indexing;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import org.drools.compiler.compiler.ReturnValueDescr;
import org.drools.compiler.lang.descr.BaseDescr;
import org.drools.compiler.lang.descr.CompositePackageDescr;
import org.drools.compiler.lang.descr.ConnectiveDescr;
import org.drools.compiler.lang.descr.OperatorDescr;
import org.drools.compiler.lang.descr.PatternSourceDescr;
import org.drools.compiler.lang.descr.ProcessDescr;
import org.drools.compiler.lang.descr.RestrictionDescr;
import org.junit.Test;
import org.kie.workbench.common.services.refactoring.backend.server.impact.ResourceReferenceCollector;
import org.mvel2.asm.ClassReader;
import org.mvel2.asm.ClassVisitor;
import org.mvel2.asm.MethodVisitor;
import org.mvel2.asm.Opcodes;
import org.reflections.Reflections;
import org.reflections.scanners.SubTypesScanner;
import org.reflections.scanners.TypeElementsScanner;
import org.reflections.util.ClasspathHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.Sets;
import com.google.common.collect.Sets.SetView;
import javassist.Modifier;
/**
* This test is focused on reflection-based code that inspects the {@link PackageDescrIndexVisitor} logic for inconsistencies.
*/
public class PackageDescrIndexVisitorLogicTest {
private static final Logger logger = LoggerFactory.getLogger(PackageDescrIndexVisitorLogicTest.class);
// @formatter:off
private Reflections descrReflections = new Reflections(
ClasspathHelper.forPackage(BaseDescr.class.getPackage().getName(), BaseDescr.class.getClassLoader()),
new TypeElementsScanner(), new SubTypesScanner());
// @formatter:on
@Test
public void visitLogicTest() {
Set<Class> visitMethodArguments = getVisitMethodArguments();
Set<Class<? extends BaseDescr>> descrClasses = descrReflections.getSubTypesOf(BaseDescr.class);
descrClasses.remove(ReturnValueDescr.class); // used in jbpm-flow
descrClasses.remove(ProcessDescr.class); // used in jbpm-flow
descrClasses.remove(CompositePackageDescr.class); // treated as a PackageDescr
descrClasses.remove(OperatorDescr.class); // no reference info
descrClasses.remove(ConnectiveDescr.class); // not enough info to reliably reference a
descrClasses.remove(PatternSourceDescr.class); // "abstract" class, even though it isn't
descrClasses.remove(RestrictionDescr.class); // "abstract" class, even though it isn't
String accDescrClassName = "org.drools.compiler.lang.descr.AccessorDescr";
try {
descrClasses.remove(Class.forName(accDescrClassName));
// the AccessorDescr class is dead code..
// we reference by name so this doesn't break when we remove the class
} catch(Exception e) {
// no-op
}
String restrictionClassName = "org.drools.compiler.lang.descr.Restriction";
try {
descrClasses.remove(Class.forName(restrictionClassName));
// the Restriction class is dead code..
// we reference by name so this doesn't break when we remove the class
} catch(Exception e) {
// no-op
}
Iterator<Class<? extends BaseDescr>> iter = descrClasses.iterator();
while (iter.hasNext()) {
Class descrImpl = iter.next();
if (Modifier.isAbstract(descrImpl.getModifiers())
|| descrImpl.isLocalClass()
|| descrImpl.isMemberClass() ) {
iter.remove();
}
}
SetView<Class<? extends BaseDescr>> diff = Sets.difference(descrClasses, visitMethodArguments);
TreeSet<Class> orderedDiff = new TreeSet<>(new ClassComparator());
orderedDiff.addAll(diff);
for (Class missingVisitMethodParam : orderedDiff) {
logger.info("Create method for: " + missingVisitMethodParam.getSimpleName() );
}
if( ! orderedDiff.isEmpty() ) {
fail( "visit(...) method missing for " + orderedDiff.iterator().next().getName());
}
}
@Test
public void visitMethodUseTest() throws IOException {
List<Class> calledVisitMethodArgumentsInObjectVisitMethod = new ArrayList<>();
ClassVisitor visitor = new ClassVisitor(Opcodes.ASM5) {
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
if (name.equals("visit") && desc.equals("(Ljava/lang/Object;)V")) {
MethodVisitor oriMv = new MethodVisitor(Opcodes.ASM5) {
@Override
public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
if (name.equals("visit")) {
desc = desc.replaceAll(".*\\(L", "");
desc = desc.replaceAll(";\\).*", "");
desc = desc.replaceAll("/", ".");
Class argClass;
try {
argClass = Class.forName(desc);
calledVisitMethodArgumentsInObjectVisitMethod.add(argClass);
} catch (ClassNotFoundException e) {
fail("Class not found: " + e.getMessage());
}
}
}
};
return oriMv;
}
return null;
}
};
String classLoc = PackageDescrIndexVisitor.class.getSimpleName() + ".class";
InputStream in = getClass().getResourceAsStream(classLoc);
assertNotNull("Resource not found: " + classLoc, in);
ClassReader classReader = new ClassReader(in);
classReader.accept(visitor, 0);
Set<Class> visitMethodArguments = getVisitMethodArguments();
Class firstErrorClass = null;
boolean fail = false;
for (Class visitMethodArgClass : visitMethodArguments) {
if( ! calledVisitMethodArgumentsInObjectVisitMethod.contains(visitMethodArgClass) ) {
if( firstErrorClass == null ) {
firstErrorClass = visitMethodArgClass;
fail = true;
}
System.out.println( visitMethodArgClass.getSimpleName() + " visit method not called in visit(Object) !" );
}
}
if( fail ) {
assertFalse( firstErrorClass.getSimpleName() + " visit method not called in visit(Object) !",
fail );
}
for( Class calledMethodArgClass : calledVisitMethodArgumentsInObjectVisitMethod ) {
assertTrue("Infinite loop detected!", visitMethodArguments.contains(calledMethodArgClass));
}
}
private Set<Class> getVisitMethodArguments() {
Method[] visitorMethods = PackageDescrIndexVisitor.class.getDeclaredMethods();
Set<Class> visitMethodArguments = new TreeSet<>(new ClassComparator());
for (Method method : visitorMethods) {
if (method.getName().equals("visit") && method.getParameterCount() == 1) {
Class paramType = method.getParameterTypes()[0];
if (BaseDescr.class.isAssignableFrom(paramType)
&& !paramType.isLocalClass()
&& !Modifier.isAbstract(paramType.getModifiers())) {
visitMethodArguments.add(paramType);
}
}
}
visitMethodArguments.remove(Object.class);
return visitMethodArguments;
}
private class ClassComparator implements Comparator<Class> {
@Override
public int compare(Class o1, Class o2) {
if (o1 == o2) {
return 0;
}
if (o2 == null) {
return 1;
} else if (o1 == null) {
return -1;
} else {
return o1.getName().compareTo(o2.getName());
}
}
}
}