使用新的 class 定义重新编译以进行变异测试

Recompile with new class definition for mutation testing

我正在尝试使用 openHFT/java-runtime-compiler 来改进我的突变测试工具,从大量使用磁盘访问到仅使用内存中编译。

在突变测试中,有两种class: 一个。变异的 class,一个 class 其定义将不断地 manipulated/altered,并重新编译。 乙。 other class,a class 其定义不会改变,即测试用例 class,或变异 class 所需的其他 class =].

通过使用 openHFT/java-runtime-compiler,可以使用下面的代码轻松完成,它是通过为变异的 class 和其他的每次重新编译创建一个新的 classLoader class。

String BSourceCode = loadFromFiles(); //class definition loaded
for( someIterationCondition ){
   String ASourceCode = mutation();  //some operation of generation/manipulation/mutation of ASourceCode
   ClassLoader classLoader = new ClassLoader() { }; 
   Class AClass = CompilerUtils.CACHED_COMPILER.loadFromJava( classLoader, "A" , ASourceCode);
   Class BClass = CompilerUtils.CACHED_COMPILER.loadFromJava( classLoader, "B" , BSourceCode);
}

效果很好,每次对classA的新定义进行编译时,AClass都会调整为新的定义。

但是,如果顺序颠倒,这将不起作用,例如下面的代码(先加载 BClass,然后加载 AClass),有时需要这样,例如当 AClass 使用 BClass 时。 class A 的重新编译不会调整到新的定义,并且将始终使用用于编译 class A.

的第一个定义
String BSourceCode = loadFromFiles(); //class definition loaded
for( someIterationCondition ){
   String ASourceCode = mutation();  //some operation of generation/manipulation/mutation of ASourceCode
   ClassLoader classLoader = new ClassLoader() { }; 
   Class BClass = CompilerUtils.CACHED_COMPILER.loadFromJava( classLoader, "B" , BSourceCode);
   Class AClass = CompilerUtils.CACHED_COMPILER.loadFromJava( classLoader, "A" , ASourceCode);
}

我怀疑我需要从 openHFT/java-runtime-compiler 库(代码下方)修改 loadFromJava class。我已经尝试省略行

//if (clazz != null)
    //return clazz;

我希望它总是在每次 loadFromJava 调用时重新编译所有源代码(甚至是已经编译的源代码)。但它给出了错误的结果。

请帮助我指出使其正常工作所需的更改。

public Class loadFromJava(@NotNull ClassLoader classLoader,
                          @NotNull String className,
                          @NotNull String javaCode,
                          @Nullable PrintWriter writer) throws ClassNotFoundException {
    Class clazz = null;
    Map<String, Class> loadedClasses;
    synchronized (loadedClassesMap) {
        loadedClasses = loadedClassesMap.get(classLoader);
        if (loadedClasses == null){
            loadedClassesMap.put(classLoader, loadedClasses = new LinkedHashMap<String, Class>());
        }else{
            clazz = loadedClasses.get(className);
        }
    }
    PrintWriter printWriter = (writer == null ? DEFAULT_WRITER : writer);
    if (clazz != null)
        return clazz;
    for (Map.Entry<String, byte[]> entry : compileFromJava(className, javaCode, printWriter).entrySet()) {
        String className2 = entry.getKey();
        synchronized (loadedClassesMap) {
            if (loadedClasses.containsKey(className2))
                continue;
        }
        byte[] bytes = entry.getValue();
        if (classDir != null) {
            String filename = className2.replaceAll("\.", '\' + File.separator) + ".class";
            boolean changed = writeBytes(new File(classDir, filename), bytes);
            if (changed) {
                LOG.info("Updated {} in {}", className2, classDir);
            }
        }
        Class clazz2 = CompilerUtils.defineClass(classLoader, className2, bytes);
        synchronized (loadedClassesMap) {
            loadedClasses.put(className2, clazz2);
        }
    }
    synchronized (loadedClassesMap) {
        loadedClasses.put(className, clazz = classLoader.loadClass(className));
    }
    return clazz;
}

非常感谢您的帮助。

已编辑

谢谢 Peter Lawrey,我试过你的建议,但它给出了相同的结果,A class 坚持使用的第一个定义(在第一次迭代中),并且未能 change/use 到新定义(在下一次迭代中)。

我收集了症状,可能的解释是第一次迭代(第一次 class 是 compiled/loaded)与下一次迭代有一些不同的处理方式。从那里我尝试了一些东西。

第一个症状

当我在 loadFromJava(下)中放置一个输出行(System.out.println)时

    Class clazz = null;
    Map<String, Class> loadedClasses;
    synchronized (loadedClassesMap) {
        loadedClasses = loadedClassesMap.get(classLoader);
        if (loadedClasses == null){
            loadedClassesMap.put(classLoader, loadedClasses = new LinkedHashMap<String, Class>());
            System.out.println("loadedClasses Null "+className);
        }else{
            clazz = loadedClasses.get(className);
            if(clazz == null)
                System.out.println("clazz Null "+className);
            else
                System.out.println("clazz not Null "+className);    
        }
    }

输出给出:

1st Iteration (new ClassLoader and new CachedCompiler)
(when loading B):loadedClasses Null
(when loading A):clazz Null

next Iteration (new ClassLoader and new CachedCompiler)
(when loading B):loadedClasses Null
(when loading A):clazz Not Null

在第一次迭代中,它给出了正确的输出,"loadClasses Null"(当加载 B 时)因为 loadedClassesMap 没有 classLoader,并给出 "clazz Null"(当加载 B 时) loading A) 因为 loadedClassesMap 有 classLoader 但没有 A classname。

但是在下一次迭代中,(加载A时)它输出"clazz Not Null",似乎A classname已经存储在loadedClassesMap.get(classLoader中),这是不应该发生的。我已尝试清除 CachedCompiler 构造函数中的 loadedClassesMap。

   loadedClassesMap.clear();

但它给出了 LinkageError: loader (instance of main/Utama$2): attempted duplicate class definition.

第二个症状

当我检查 s_fileManager 缓冲区时,第一次迭代中差异化的更强烈症状。

1st Iteration (new ClassLoader and new CachedCompiler)
(when load B):CompilerUtils.s_fileManager.getAllBuffers().size()=1
(when load A):CompilerUtils.s_fileManager.getAllBuffers().size()=2

Next Iteration (new ClassLoader and new CachedCompiler)
(when load B):CompilerUtils.s_fileManager.getAllBuffers().size()=2
(when load A):CompilerUtils.s_fileManager.getAllBuffers().size()=2

第一次迭代符合预期,但在下一次迭代中,s_fileManager 缓冲区似乎已经达到大小 2,并且没有重置为 0。

我已尝试在 CachedCompiler 构造函数(下方)中清除 FileManager 缓冲区,

CompilerUtils.s_fileManager.clearBuffers();

但它给出了 ExceptionInInitializerError。

如果您想使用一组全新的 classes,我建议您不要使用与 classes 相同的缓存。

String BSourceCode = loadFromFiles(); //class definition loaded
for( someIterationCondition ){
   String ASourceCode = mutation();  //some operation of generation/manipulation/mutation of ASourceCode
   ClassLoader classLoader = new ClassLoader() { }; 
   CachedCompiler compiler = new CachedCompiler(null, null)
   Class AClass = compiler.loadFromJava( classLoader, "A" , ASourceCode);
   Class BClass = compiler.loadFromJava( classLoader, "B" , BSourceCode);
}

这将每次使用新的缓存,并且不受之前测试中加载的 class 的影响。