Search code examples
chroniclemutation-testingjava-runtime-compiler

Recompile with new class definition for mutation testing


I am trying to use openHFT/java-runtime-compiler to improve my mutation testing tools, from heavy use of disk-access to only use in-memory-compilation.

In mutation testing, there was a two kind of class: A. the mutated class, a class that its definition will be constantly manipulated/altered, and recompiled. B. other class, a class that its definition will not be change, i.e. test case class, or other class that needed by the mutated class.

By using openHFT/java-runtime-compiler, it can be easily done by using below code, it was by creating a new classLoader for each recompilation of both the mutated class, and other 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);
}

That works well, each time a new definition of class A compiled, the AClass will adjust to the new definition.

But, this wouldn't work if the sequence was reversed, like in below code (BClass loaded first then AClass), which sometimes needed, like when the AClass use BClass. The recompilation of class A, would not adjust to the new definition, and will always use the first definition that used to compile 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);
}

I suspect that I needed to modify the loadFromJava class from openHFT/java-runtime-compiler library (below code). I already try by omitting the lines

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

that I expected to make it always recompiled all the source code (even the already compiled one) for every times loadFromJava called. But it gives wrong results.

Please help me to point out the change needed to make it to works.

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;
}

Thank you very much for all your help.

Edited

Thanks Peter Lawrey, I have tried your suggestion, but its give the same result, the A class is stick to the first definition used (in the first iteration), and fail to change/use to a new definition (in the next iteration).

I have gathered symptom's and the possible explanation was there were some different treating of the first iteration (first time class was compiled/loaded) than the next iteration. From there I try a couple thing.

1st Symptoms

It was when I put an output line (System.out.println) in the loadFromJava (below)

    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);    
        }
    }

the output gives:

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

in the first iteration, it was giving the right output, "loadClasses Null" (when loading B) because loadedClassesMap doesn't have the classLoader, and give "clazz Null" (when loading A) because the loadedClassesMap have the classLoader but doesn't have the A classname.

However in the next iteration, (when loading A) it output "clazz Not Null", it seems A classname already stored in loadedClassesMap.get(classLoader), which is not supposed to happen. I have try to clear the loadedClassesMap in the CachedCompiler constructor.

   loadedClassesMap.clear();

but it gives LinkageError: loader (instance of main/Utama$2): attempted duplicate class definition.

2nd symptomps

the more strong symptoms of the differentiation in the first iteration was when I check the s_fileManager buffer.

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

The 1st iteration was as expected, but in the next Iteration, s_fileManager buffer seems to already got size 2, and not reset to 0.

I have tried to clear FileManager Buffer at CachedCompiler constructor (below),

CompilerUtils.s_fileManager.clearBuffers();

but it gives ExceptionInInitializerError.


Solution

  • If you want to use a fresh set of classes, I suggest not using the same cache of 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);
    }
    

    This will use a new cache each time and not be affected by class loaded in a previous test.