diff --git a/java/execution/impl/execution-impl.iml b/java/execution/impl/execution-impl.iml index bf85ee694ec4..ed11ab8cf3e1 100644 --- a/java/execution/impl/execution-impl.iml +++ b/java/execution/impl/execution-impl.iml @@ -23,7 +23,9 @@ - + + + diff --git a/java/execution/impl/testDiscovery/build.xml b/java/execution/impl/testDiscovery/build.xml new file mode 100644 index 000000000000..e86f1ceb6043 --- /dev/null +++ b/java/execution/impl/testDiscovery/build.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/java/execution/impl/testDiscovery/org/jetbrains/testme/instrumentation/CoveragePremain.java b/java/execution/impl/testDiscovery/org/jetbrains/testme/instrumentation/CoveragePremain.java new file mode 100755 index 000000000000..3cce29a2038d --- /dev/null +++ b/java/execution/impl/testDiscovery/org/jetbrains/testme/instrumentation/CoveragePremain.java @@ -0,0 +1,76 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.jetbrains.testme.instrumentation; + + +import java.io.File; +import java.lang.instrument.Instrumentation; +import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; + +public class CoveragePremain { + + public static void premain(String argsString, Instrumentation instrumentation) throws Exception { + final File lib = new File(getArchivePath()).getParentFile(); + final URL[] urls = new URL[3]; + urls[0] = fileToURL(new File(lib, "testDiscoveryInstrumenter.jar")); + urls[1] = fileToURL(new File(lib, "asm-all.jar")); + urls[2] = fileToURL(new File(lib, "trove4j.jar")); + + final Class instrumentator = Class.forName("org.jetbrains.testme.instrumentation.TestDiscoveryInstrumentator", true, new URLClassLoader(urls) { + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + synchronized (this) { + Class result = findLoadedClass(name); + if (result == null) { + try { + result = findClass(name); + } catch (ClassNotFoundException e) { + //ignore, will try to find class in parent + } + } + + if (result != null && resolve) { + resolveClass(result); + } + + if (result != null) { + return result; + } + } + + return getParent().loadClass(name); + } + }); + final Method premainMethod = instrumentator.getDeclaredMethod("premain", new Class[]{String.class, Instrumentation.class}); + premainMethod.invoke(null, new Object[] {argsString, instrumentation}); + } + + private static URL fileToURL(final File file) throws MalformedURLException { + return file.getAbsoluteFile().toURI().toURL(); + } + + private static String getArchivePath() { + final String className = CoveragePremain.class.getName().replace('.', '/') + ".class"; + URL resourceURL = CoveragePremain.class.getResource("/" + className); + if (resourceURL == null) { + resourceURL = ClassLoader.getSystemResource(className); + } + return URLsUtil.extractRoot(resourceURL, "/" + className); + } +} \ No newline at end of file diff --git a/java/execution/impl/testDiscovery/org/jetbrains/testme/instrumentation/InstrumentedMethodsFilter.java b/java/execution/impl/testDiscovery/org/jetbrains/testme/instrumentation/InstrumentedMethodsFilter.java new file mode 100644 index 000000000000..5786c953542a --- /dev/null +++ b/java/execution/impl/testDiscovery/org/jetbrains/testme/instrumentation/InstrumentedMethodsFilter.java @@ -0,0 +1,37 @@ +package org.jetbrains.testme.instrumentation; + +import org.jetbrains.org.objectweb.asm.Opcodes; + +public class InstrumentedMethodsFilter { + private final String myClassName; + private boolean myEnum; + + public InstrumentedMethodsFilter(String className) { + myClassName = className; + } + + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + myEnum = (access & Opcodes.ACC_ENUM) != 0; + } + + public boolean shouldVisitMethod(final int access, + final String name, + final String desc, + final String signature, + final String[] exceptions) { + if ((access & Opcodes.ACC_BRIDGE) != 0) return false; //try to skip bridge methods + if ((access & Opcodes.ACC_ABSTRACT) != 0) return false; //skip abstracts; do not include interfaces without non-abstract methods in result + if ("".equals(name) || "".equals(name)) return false; //skip (static/instance) initializers + + if (myEnum && isDefaultEnumMethod(name, desc, signature, myClassName)) { + return false; + } + return true; + } + + private static boolean isDefaultEnumMethod(String name, String desc, String signature, String className) { + return name.equals("values") && desc.equals("()[L" + className + ";") || + name.equals("valueOf") && desc.equals("(Ljava/lang/String;)L" + className + ";") || + name.equals("") && signature != null && signature.equals("()V"); + } +} diff --git a/java/execution/impl/testDiscovery/org/jetbrains/testme/instrumentation/Instrumenter.java b/java/execution/impl/testDiscovery/org/jetbrains/testme/instrumentation/Instrumenter.java new file mode 100755 index 000000000000..76f9c673c9e2 --- /dev/null +++ b/java/execution/impl/testDiscovery/org/jetbrains/testme/instrumentation/Instrumenter.java @@ -0,0 +1,148 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.jetbrains.testme.instrumentation; + +import org.jetbrains.org.objectweb.asm.ClassVisitor; +import org.jetbrains.org.objectweb.asm.Label; +import org.jetbrains.org.objectweb.asm.MethodVisitor; +import org.jetbrains.org.objectweb.asm.Opcodes; + +public class Instrumenter extends ClassVisitor { + protected final ClassVisitor myClassVisitor; + private final String myClassName; + private final String myInternalClassName; + private final InstrumentedMethodsFilter myMethodFilter; + private final String[] myMethodNames; + private int myCurrentMethodCount; + private boolean myVisitedStaticBlock; + + private static final String METHODS_VISITED = "__$methodsVisited$__"; + private static final String METHODS_VISITED_CLASS = "[Z"; + + public Instrumenter(ClassVisitor classVisitor, String className, String[] methodNames) { + super(Opcodes.ASM5, classVisitor); + myClassVisitor = classVisitor; + myMethodFilter = new InstrumentedMethodsFilter(className); + myClassName = className.replace('$', '.'); // for inner classes + myInternalClassName = className.replace('.', '/'); + myMethodNames = methodNames; + } + + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + myMethodFilter.visit(version, access, name, signature, superName, interfaces); + super.visit(version, access, name, signature, superName, interfaces); + } + + public MethodVisitor visitMethod(final int access, + final String name, + final String desc, + final String signature, + final String[] exceptions) { + final MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions); + if (mv == null) return mv; + if ("".equals(name)) { + myVisitedStaticBlock = true; + return new StaticBlockMethodVisitor(mv); + } + + if (!myMethodFilter.shouldVisitMethod(access, name, desc, signature, exceptions)) return mv; + + assert myCurrentMethodCount < myMethodNames.length; + + return new MethodVisitor(Opcodes.ASM5, mv) { + final int myMethodId = myCurrentMethodCount++; + + public void visitCode() { + visitFieldInsn(Opcodes.GETSTATIC, myInternalClassName, METHODS_VISITED, METHODS_VISITED_CLASS); + pushInstruction(this, myMethodId); + visitInsn(Opcodes.ICONST_1); + visitInsn(Opcodes.BASTORE); + + super.visitCode(); + } + }; + } + + @Override + public void visitEnd() { + visitField(Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL | Opcodes.ACC_STATIC, METHODS_VISITED, + METHODS_VISITED_CLASS, null, null); + + if (!myVisitedStaticBlock) { + MethodVisitor mv = super.visitMethod(Opcodes.ACC_STATIC, "", "()V", null, null); + mv = new StaticBlockMethodVisitor(mv); + mv.visitCode(); + mv.visitInsn(Opcodes.RETURN); + mv.visitMaxs(myMethodNames.length + 2, 1); + mv.visitEnd(); + } + super.visitEnd(); + } + + private class StaticBlockMethodVisitor extends MethodVisitor { + public StaticBlockMethodVisitor(MethodVisitor mv) { + super(Opcodes.ASM5, mv); + } + + public void visitCode() { + super.visitCode(); + + pushInstruction(this, myMethodNames.length); + visitIntInsn(Opcodes.NEWARRAY, Opcodes.T_BOOLEAN); + visitFieldInsn(Opcodes.PUTSTATIC, myInternalClassName, METHODS_VISITED, METHODS_VISITED_CLASS); + + pushInstruction(this, myMethodNames.length); + + visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/String"); + + for(int i = 0; i < myMethodNames.length; ++i) { + visitInsn(Opcodes.DUP); + pushInstruction(this, i); + visitLdcInsn(myMethodNames[i]); + visitInsn(Opcodes.AASTORE); + } + + visitVarInsn(Opcodes.ASTORE, 0); + + Label startLabel = new Label(); + visitLabel(startLabel); + + visitLdcInsn(myClassName); + visitFieldInsn(Opcodes.GETSTATIC, myInternalClassName, METHODS_VISITED, METHODS_VISITED_CLASS); + visitVarInsn(Opcodes.ALOAD, 0); + visitMethodInsn(Opcodes.INVOKESTATIC, ProjectData.PROJECT_DATA_OWNER, "trace", "(Ljava/lang/String;[Z[Ljava/lang/String;)V", false); + + Label endLabel = new Label(); + visitLabel(endLabel); + + visitLocalVariable("methodNames", "[Ljava/lang/String;", null, startLabel, endLabel, 0); + // no return here + } + + public void visitMaxs(int maxStack, int maxLocals) { + final int ourMaxStack = myMethodNames.length + 2; + final int ourMaxLocals = 1; + + super.visitMaxs(Math.max(ourMaxStack, maxStack), Math.max(ourMaxLocals, maxLocals)); + } + } + + private static void pushInstruction(MethodVisitor mv, int operand) { + if (operand < Byte.MAX_VALUE) mv.visitIntInsn(Opcodes.BIPUSH, operand); + else mv.visitIntInsn(Opcodes.SIPUSH, operand); + } +} \ No newline at end of file diff --git a/java/execution/impl/testDiscovery/org/jetbrains/testme/instrumentation/ProjectData.java b/java/execution/impl/testDiscovery/org/jetbrains/testme/instrumentation/ProjectData.java new file mode 100644 index 000000000000..31336bd6e1fc --- /dev/null +++ b/java/execution/impl/testDiscovery/org/jetbrains/testme/instrumentation/ProjectData.java @@ -0,0 +1,158 @@ +package org.jetbrains.testme.instrumentation; + +import java.io.*; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; + +public class ProjectData { + public static final String PROJECT_DATA_OWNER = "org/jetbrains/testme/instrumentation/ProjectData"; + public static final String TRACE_DIR = "org.jetbrains.testme.instrumentation.trace.dir"; + + protected static final ProjectData ourData = new ProjectData(); + + private String myTraceDir = System.getProperty(TRACE_DIR, ""); + + public void setTraceDir(String traceDir) { + myTraceDir = traceDir; + } + + private ConcurrentMap> myTrace; + private final ConcurrentMap myTrace2 = new ConcurrentHashMap(); + private final ConcurrentMap myTrace3 = new ConcurrentHashMap(); + + public static ProjectData getProjectData() { + return ourData; + } + + public static void trace(String className, String methodSignature) { + ourData.traceLines(className, methodSignature); + } + + // called from instrumented code during class's static init + public static void trace(String className, boolean[] methodFlags, String[] methodNames) { + ourData.traceLines(className, methodFlags, methodNames); + } + + public void traceLines(String className, String methodSignature) { + if (myTrace != null) { + Set methods = myTrace.get(className); + if (methods == null) { + methods = new HashSet(); + Set previousMethods = myTrace.putIfAbsent(className, methods); + if (previousMethods != null) methods = previousMethods; + } + synchronized (methods) { + methods.add(methodSignature); + } + } + } + + public synchronized void traceLines(String className, boolean[] methodFlags, String[] methodNames) { + //System.out.println("Registering " + className); + assert methodFlags.length == methodNames.length; + myTrace2.put(className, methodFlags); + myTrace3.put(className, methodNames); + } + + private static volatile boolean traceDirDumped; + + public synchronized void testEnded(final String name) { + //if (myTrace == null) return; + if (!traceDirDumped) { + ClassLoader classLoader = TestDiscoveryInstrumentator.class.getClassLoader(); + System.out.println(ourData + "; cl: " + classLoader+ "," + classLoader.getParent()); + System.out.println("Trace dir:" + myTraceDir); + traceDirDumped = true; + } + new File(myTraceDir).mkdirs(); + final File traceFile = new File(myTraceDir, name + ".tr"); + try { + if (!traceFile.exists()) { + traceFile.createNewFile(); + } + DataOutputStream os = null; + Deflater def = new Deflater(1); + try { + os = new DataOutputStream(new DeflaterOutputStream(new BufferedOutputStream(new FileOutputStream(traceFile)), def)); + + //saveOldTrace(os); + + Map classToUsedMethods = new HashMap(); + for(Map.Entry e: myTrace2.entrySet()) { + boolean[] used = e.getValue(); + int usedMethodsCount = 0; + + for (boolean anUsed : used) { + if (anUsed) ++usedMethodsCount; + } + + if (usedMethodsCount > 0) { + classToUsedMethods.put(e.getKey(), usedMethodsCount); + } + } + + os.writeInt(classToUsedMethods.size()); + for(Map.Entry e: myTrace2.entrySet()) { + final boolean[] used = e.getValue(); + final String className = e.getKey(); + + Integer integer = classToUsedMethods.get(className); + if (integer == null) continue;; + + int usedMethodsCount = integer; + + os.writeUTF(className); + os.writeInt(usedMethodsCount); + + String[] methodNames = myTrace3.get(className); + for (int i = 0, len = used.length; i < len; ++i) { + // we check usedMethodCount here since used was observed to change // ? + if (used[i] && usedMethodsCount-- > 0) os.writeUTF(methodNames[i]); + } + } + } + finally { + if (os != null) { + os.close(); + } + def.end(); + } + } + catch (IOException e) { + e.printStackTrace(); + } + finally { + myTrace = null; + } + } + + private void saveOldTrace(DataOutputStream os) throws IOException { + os.writeInt(myTrace.size()); + for (Iterator it = myTrace.keySet().iterator(); it.hasNext();) { + final String classData = it.next(); + os.writeUTF(classData); + final Set methods = myTrace.get(classData); + os.writeInt(methods.size()); + for (Iterator iterator = methods.iterator(); iterator.hasNext(); ) { + os.writeUTF(iterator.next()); + } + } + } + + public synchronized void testStarted(final String name) { + //clearOldTrace(); + for(Map.Entry e: myTrace2.entrySet()) { + boolean[] used = e.getValue(); + for(int i = 0, len = used.length; i < len; ++i) { + if(used[i]) used[i] = false; + } + } + } + + private void clearOldTrace() { + myTrace = new ConcurrentHashMap>(); + } +} diff --git a/java/execution/impl/testDiscovery/org/jetbrains/testme/instrumentation/TestDiscoveryInstrumentator.java b/java/execution/impl/testDiscovery/org/jetbrains/testme/instrumentation/TestDiscoveryInstrumentator.java new file mode 100755 index 000000000000..71d91feb7743 --- /dev/null +++ b/java/execution/impl/testDiscovery/org/jetbrains/testme/instrumentation/TestDiscoveryInstrumentator.java @@ -0,0 +1,265 @@ +/* + * Copyright 2000-2015 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.jetbrains.testme.instrumentation; + +import org.jetbrains.org.objectweb.asm.*; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.IllegalClassFormatException; +import java.lang.instrument.Instrumentation; +import java.security.ProtectionDomain; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + + +public class TestDiscoveryInstrumentator { + + public static void premain(String argsString, Instrumentation instrumentation) throws Exception { + instrumentation.addTransformer(new ClassFileTransformer() { + private boolean computeFrames = computeFrames(); + + public byte[] transform(ClassLoader loader, + String className, + Class classBeingRedefined, + ProtectionDomain protectionDomain, + byte[] classfileBuffer) throws IllegalClassFormatException { + try { + if (className == null) { + return null; + } + if (loader == null) { + // skip classes loaded by system classloader + //System.out.println("Skipping " + className); + return null; + } + if (className.endsWith(".class")) { + className = className.substring(0, className.length() - 6); + } + className = className.replace('\\', '.').replace('/', '.'); + + if (className.startsWith("com.intellij.rt.") + || className.startsWith("com.intellij.util.lang.") + || className.startsWith("com.intellij.util.containers.") + || className.startsWith("com.intellij.openapi.util.text.") + || className.startsWith("com.intellij.openapi.util.io.") + || className.startsWith("java.") + || className.startsWith("sun.") + || className.startsWith("gnu.trove.") + || className.startsWith("org.jetbrains.org.objectweb.asm.") + || className.startsWith("org.apache.oro.text.regex.") + || className.startsWith("org.jetbrains.testme.") + || className.startsWith("org.junit.") + || className.startsWith("com.sun.") + || className.startsWith("junit.") + || className.startsWith("jdk.internal.") + || className.startsWith("com.intellij.junit3.") + || className.startsWith("com.intellij.junit4.")) { + return null; + } + //System.out.println(className); + return instrument(classfileBuffer, className, loader, computeFrames); + } catch (Throwable e) { + e.printStackTrace(); + } + return null; + } + + private boolean computeFrames() { + return System.getProperty("idea.coverage.no.frames") == null; + } + }); + } + + private final static AtomicInteger myInstrumentedClasses = new AtomicInteger(); + private final static AtomicInteger myInstrumentedMethods = new AtomicInteger(); + private final static AtomicLong myInstrumentedClassesTime = new AtomicLong(); + + private static byte[] instrument(final byte[] classfileBuffer, final String className, ClassLoader loader, boolean computeFrames) { + long started = System.nanoTime(); + final ClassReader cr = new ClassReader(classfileBuffer); + final ClassWriter cw; + if (computeFrames && false) { + final int version = getClassFileVersion(cr); + cw = getClassWriter(version >= Opcodes.V1_6 && version != Opcodes.V1_1 ? ClassWriter.COMPUTE_FRAMES : ClassWriter.COMPUTE_MAXS, loader); + } else { + cw = getClassWriter(ClassWriter.COMPUTE_MAXS, loader); + } + + final List instrumentedMethods = new ArrayList(); + + final ClassVisitor instrumentedMethodCounter = new ClassVisitor(Opcodes.ASM5) { + final InstrumentedMethodsFilter methodsFilter = new InstrumentedMethodsFilter(className); + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + methodsFilter.visit(version, access, name, signature, superName, interfaces); + super.visit(version, access, name, signature, superName, interfaces); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { + if (methodsFilter.shouldVisitMethod(access, name, desc, signature, exceptions)) { + instrumentedMethods.add(name); + } + return super.visitMethod(access, name, desc, signature, exceptions); + } + }; + + cr.accept(instrumentedMethodCounter, 0); + + // todo there are duplicates in array of instrumented methods + final ClassVisitor cv = new Instrumenter(cw, className, instrumentedMethods.toArray(new String[instrumentedMethods.size()])); + cr.accept(cv, 0); + byte[] bytes = cw.toByteArray(); + + long time = myInstrumentedClassesTime.addAndGet(System.nanoTime() - started); + int classes = myInstrumentedClasses.incrementAndGet(); + int methods = myInstrumentedMethods.addAndGet(instrumentedMethods.size()); + if (classes % 1000 == 0) { + System.out.println("Done instrumenting " + classes + ", methods:" + methods + " for " + (time / 1000000)); + } + + if (false) { + try { + FileOutputStream fileOutputStream = new FileOutputStream("transformed-" + className); + try { + fileOutputStream.write(bytes); + fileOutputStream.close(); + } finally { + fileOutputStream.close(); + } + } catch (IOException ex) { + ex.printStackTrace(); + } + } + return bytes; + } + + private static ClassWriter getClassWriter(int flags, final ClassLoader classLoader) { + return new MyClassWriter(flags, classLoader); + } + + public static int getClassFileVersion(ClassReader reader) { + final int[] classFileVersion = new int[1]; + reader.accept(new ClassVisitor(Opcodes.ASM5) { + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + classFileVersion[0] = version; + } + }, 0); + return classFileVersion[0]; + } + + private static class MyClassWriter extends ClassWriter { + public static final String JAVA_LANG_OBJECT = "java/lang/Object"; + private final ClassLoader classLoader; + + public MyClassWriter(int flags, ClassLoader classLoader) { + super(flags); + this.classLoader = classLoader; + } + + protected String getCommonSuperClass(String type1, String type2) { + try { + ClassReader info1 = typeInfo(type1); + ClassReader info2 = typeInfo(type2); + String + superType = checkImplementInterface(type1, type2, info1, info2); + if (superType != null) return superType; + superType = checkImplementInterface(type2, type1, info2, info1); + if (superType != null) return superType; + + StringBuilder b1 = typeAncestors(type1, info1); + StringBuilder b2 = typeAncestors(type2, info2); + String result = JAVA_LANG_OBJECT; + int end1 = b1.length(); + int end2 = b2.length(); + while (true) { + int start1 = b1.lastIndexOf(";", end1 - 1); + int start2 = b2.lastIndexOf(";", end2 - 1); + if (start1 != -1 && start2 != -1 && end1 - start1 == end2 - start2) { + String p1 = b1.substring(start1 + 1, end1); + String p2 = b2.substring(start2 + 1, end2); + if (p1.equals(p2)) { + result = p1; + end1 = start1; + end2 = start2; + } else { + return result; + } + } else { + return result; + } + } + } catch (IOException e) { + throw new RuntimeException(e.toString()); + } + } + + private String checkImplementInterface(String type1, String type2, ClassReader info1, ClassReader info2) throws IOException { + if ((info1.getAccess() & Opcodes.ACC_INTERFACE) != 0) { + if (typeImplements(type2, info2, type1)) { + return type1; + } + return JAVA_LANG_OBJECT; + } + return null; + } + + private StringBuilder typeAncestors(String type, ClassReader info) throws IOException { + StringBuilder b = new StringBuilder(); + while (!JAVA_LANG_OBJECT.equals(type)) { + b.append(';').append(type); + type = info.getSuperName(); + info = typeInfo(type); + } + return b; + } + + private boolean typeImplements(String type, ClassReader classReader, String interfaceName) throws IOException { + while (!JAVA_LANG_OBJECT.equals(type)) { + String[] itfs = classReader.getInterfaces(); + for (int i = 0; i < itfs.length; ++i) { + if (itfs[i].equals(interfaceName)) { + return true; + } + } + for (int i = 0; i < itfs.length; ++i) { + if (typeImplements(itfs[i], typeInfo(itfs[i]), interfaceName)) { + return true; + } + } + type = classReader.getSuperName(); + classReader = typeInfo(type); + } + return false; + } + + private ClassReader typeInfo(final String type) throws IOException { + InputStream is = classLoader.getResourceAsStream(type + ".class"); + if (is == null) System.out.println(classLoader + "," + type + ".class"); + try { + return new ClassReader(is); + } finally { + is.close(); + } + } + } +} diff --git a/java/execution/impl/testDiscovery/org/jetbrains/testme/instrumentation/URLsUtil.java b/java/execution/impl/testDiscovery/org/jetbrains/testme/instrumentation/URLsUtil.java new file mode 100644 index 000000000000..05510fe75f45 --- /dev/null +++ b/java/execution/impl/testDiscovery/org/jetbrains/testme/instrumentation/URLsUtil.java @@ -0,0 +1,137 @@ +/* + * Copyright 2000-2014 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.jetbrains.testme.instrumentation; + +import java.io.File; +import java.io.UnsupportedEncodingException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +public class URLsUtil { + public static final String FILE = "file"; + public static final String PROTOCOL_DELIMITER = ":"; + public static final String JAR_DELIMITER = "!"; + + public static boolean startsWithChar(CharSequence s, char prefix) { +return s != null && s.length() != 0 && s.charAt(0) == prefix; +} + + public static String extractRoot(URL resourceURL, String resourcePath) { + if (!(startsWithChar(resourcePath, '/') || startsWithChar(resourcePath, '\\'))) { + //noinspection HardCodedStringLiteral + System.err.println("precondition failed: "+resourcePath); + return null; + } + String protocol = resourceURL.getProtocol(); + String resultPath = null; + + if (FILE.equals(protocol)) { + String path = resourceURL.getFile(); + final String testPath = path.replace('\\', '/'); + final String testResourcePath = resourcePath.replace('\\', '/'); + if (endsWithIgnoreCase(testPath, testResourcePath)) { + resultPath = path.substring(0, path.length() - resourcePath.length()); + } + } + else if ("jar".equals(protocol)) { + String fullPath = resourceURL.getFile(); + int delimiter = fullPath.indexOf(JAR_DELIMITER); + if (delimiter >= 0) { + String archivePath = fullPath.substring(0, delimiter); + if (startsWithConcatenationOf(archivePath, FILE, PROTOCOL_DELIMITER)) { + resultPath = archivePath.substring(FILE.length() + PROTOCOL_DELIMITER.length()); + } + } + } + if (resultPath == null) { + //noinspection HardCodedStringLiteral + System.err.println("cannot extract: "+resultPath + " from "+resourceURL); + return null; + } + + if (resourcePath.endsWith(File.separator)) { + resultPath = resultPath.substring(0, resultPath.lastIndexOf(File.separator)); + } + resultPath = unescapePercentSequences(resultPath); + return resultPath; + } + + public static boolean startsWithConcatenationOf(String testee, String firstPrefix, String secondPrefix) { + int l1 = firstPrefix.length(); + int l2 = secondPrefix.length(); + if (testee.length() < l1 + l2) return false; + return testee.startsWith(firstPrefix) && testee.regionMatches(l1, secondPrefix, 0, l2); + } + + public static boolean endsWithIgnoreCase(String str, String suffix) { + final int stringLength = str.length(); + final int suffixLength = suffix.length(); + return stringLength >= suffixLength && str.regionMatches(true, stringLength - suffixLength, suffix, 0, suffixLength); + } + + public static String unescapePercentSequences(String s) { + if (s.indexOf('%') == -1) { + return s; + } + + StringBuilder decoded = new StringBuilder(); + final int len = s.length(); + int i = 0; + while (i < len) { + char c = s.charAt(i); + if (c == '%') { + List bytes = new ArrayList(); + while (i + 2 < len && s.charAt(i) == '%') { + final int d1 = decode(s.charAt(i + 1)); + final int d2 = decode(s.charAt(i + 2)); + if (d1 != -1 && d2 != -1) { + bytes.add(new Integer(((d1 & 0xf) << 4 | d2 & 0xf))); + i += 3; + } else { + break; + } + } + if (!bytes.isEmpty()) { + final byte[] bytesArray = new byte[bytes.size()]; + for (int j = 0; j < bytes.size(); j++) { + bytesArray[j] = (byte) ((Integer) bytes.get(j)).intValue(); + } + try { + decoded.append(new String(bytesArray, "UTF-8")); + continue; + } + catch (UnsupportedEncodingException ignored) { + } + } + } + + decoded.append(c); + i++; + } + return decoded.toString(); + } + + private static int decode(char c) { + if ((c >= '0') && (c <= '9')) + return c - '0'; + if ((c >= 'a') && (c <= 'f')) + return c - 'a' + 10; + if ((c >= 'A') && (c <= 'F')) + return c - 'A' + 10; + return -1; + } +} \ No newline at end of file