package ca.uwaterloo.ece.qhanam.jrsrepair;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Stack;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import ca.uwaterloo.ece.qhanam.jrsrepair.compiler.JavaJDKCompiler;
import ca.uwaterloo.ece.qhanam.jrsrepair.context.Context;
import ca.uwaterloo.ece.qhanam.jrsrepair.context.MutationContext;
import ca.uwaterloo.ece.qhanam.jrsrepair.context.MutationContext.MutationType;
import ca.uwaterloo.ece.qhanam.jrsrepair.mutation.*;
public class JRSRepair {
/* Provides context for all tasks: repair, parsing, mutation, compilation and testing. */
private Context context;
/* Keep track of the mutation operations for logging. */
private Stack<String> patches;
/**
* Creates a JRSRepair object with the path to the source folder
* of the program we are mutating.
* @param sourcepaths The path to the source folder of the program we are mutating.
*/
public JRSRepair(Context context) throws Exception {
/* The context for this repair. It's super important that this is set
* up properly! */
this.context = context;
/* Initialize the patch-building stack. */
this.patches = new Stack<String>();
}
/**
* Builds ASTs for all the source files. Must be called before repair().
*
* We could do this in the constructor
* of ParserContest, but this operation takes a while, so we leave it here
* so it is easier to print progress to the user.
*/
public void buildASTs() throws Exception{
this.context.parser.buildASTs();
}
/**
* Attempts to repair the program using the RSRepair method.
* @throws Exception
*/
public void repair() throws Exception{
try{
for(int i = 0; i < this.context.repair.candidates; i++) {
System.out.println("Running candidate " + (i + 1) + " ...");
this.mutationIteration(i + 1, 1);
}
System.out.println("Finished!");
}
catch(Exception e){
System.out.println("Error: " + e.getMessage());
throw e;
}
}
/**
* The main method for trying a mutation. It performs all the operations needed
* to mutate, compile and test the program. It is recursive and will therefore
* attempt multiple mutations at a time before rolling back their changes.
*
* @param candidate The identifier for the current set of mutations.
* @param generation The number of mutations that have already been applied for the current candidate.
*/
private void mutationIteration(int candidate, int generation) throws Exception{
/* If we can't find a solution within some number of iterations, abort. */
int attemptCounter = 0;
/* Let the user know our progress. */
System.out.println("Running generation " + generation + " ...");
/* Select the mutation type to use for this generation. */
MutationContext.MutationType mutationType;
if(this.context.repair.nullMutationOnlly){
/* Do not mutate. */
mutationType = MutationContext.MutationType.NULL;
}
else {
mutationType = this.context.mutation.getRandomMutationType();
}
Mutation mutation = null;
JavaJDKCompiler.Status compileStatus;
AbstractTestExecutor.Status testStatus;
/* The compiler loop. Attempt to compile until the counter reaches max
* attempts set by the user. */
do {
try {
/* First we need to get a mutation that will likely compile. To do this
* we randomly select a mutation, apply the mutation to the AST to get
* a new document, parse the new document into an AST (having the parser
* attempt to resolve bindings) and finally check that all variables have
* bindings.*/
int ctr = 0;
while(true){
/* Get a random mutation operation to apply. */
mutation = this.context.mutation.getRandomMutation(mutationType);
/* Apply the mutation to the AST + Document. */
mutation.mutate();
/* Check if all the variables are in scope in the new AST. */
if(this.context.parser.checkScope(mutation.getRewriter())) break;
mutation.undo();
/* Just in case... we should make sure we don't have an infinite loop. */
ctr++;
if(ctr > 1000) throw new Exception("Mutation search timed out after 1000 attempts without a passing scope check.");
}
this.logMutation(mutation, candidate, generation);
/* Now that we have a mutation that is in-scope, we attempt to compile
* the program. If the program compiles, we run the test cases. If it
* doesn't, we roll back the changes and loop to get another mutation. */
compileStatus = this.context.compiler.compile();
this.logCompileError(candidate, generation, this.context.compiler.dequeueCompileError());
/* Did it compile? If it didn't we might need to undo the mutation before trying again.
* Either way, log what happened. */
if(compileStatus == JavaJDKCompiler.Status.NOT_COMPILED && this.context.repair.revertFailedCompile) {
System.out.print(" - Did not compile\n");
mutation.undo();
} else if(compileStatus == JavaJDKCompiler.Status.NOT_COMPILED) {
this.patches.push("Candidate " + candidate + ", Generation " + generation + "\n" + mutation.toString());
System.out.print(" - Did not compile\n");
} else {
this.patches.push("Candidate " + candidate + ", Generation " + generation + "\n" + mutation.toString());
System.out.print(" - Compiled!");
}
}
catch (Exception e) {
/* The scope checking phase may have timed out. Log and move to the next attempt. */
System.out.print(e.getMessage() + "\n");
compileStatus = JavaJDKCompiler.Status.NOT_COMPILED;
/* If we are not reverting failed compiles, replace this mutation with the null mutation. */
if(!this.context.repair.revertFailedCompile) {
/* Get a random mutation operation to apply. */
mutation = this.context.mutation.getRandomMutation(MutationType.NULL);
/* Apply the mutation to the AST + Document. */
mutation.mutate();
this.patches.push("Candidate " + candidate + ", Generation " + generation + "\n" + mutation.toString());
}
}
attemptCounter++;
} while(compileStatus == JavaJDKCompiler.Status.NOT_COMPILED && attemptCounter < this.context.repair.attempts);
/* Did the program compile? If it did, run the test cases. */
if(compileStatus == JavaJDKCompiler.Status.COMPILED){
/* We may also need to copy the .class files back to their
* class folders (for example, if we have a complex Maven
* this just makes life easier than re-building ourselves). */
if(this.context.repair.classDirectories.length > 0){
for(String directory : this.context.repair.classDirectories){
Utilities.copyFiles(new File(this.context.repair.buildDirectory.getPath() + "/classes"), new File(directory));
}
}
/* Run the test cases. */
testStatus = this.context.test.runTests();
/* Log what happened. If all tests passed, store the class files. */
if(testStatus == AbstractTestExecutor.Status.PASSED) {
this.logSuccesfullPatch(candidate, generation);
System.out.print(" Passed!\n");
}
else if(testStatus == AbstractTestExecutor.Status.FAILED)
System.out.print(" Failed.\n");
else if(testStatus == AbstractTestExecutor.Status.ERROR)
System.out.print(" Error - tests may not have run.\n");
}
/* Recurse to the next level of mutations. */
if(generation < this.context.repair.generations){
this.mutationIteration(candidate, generation + 1);
}
/* Since this.patches in a field, we need to unwind the patch stack. */
if(compileStatus == JavaJDKCompiler.Status.COMPILED || !this.context.repair.revertFailedCompile) {
this.patches.pop();
mutation.undo();
}
}
/**
* Writes the mutation operations to a file. These represent a (successful?) fix.
* @throws Exception
*/
private void logSuccesfullPatch(int candidate, int generation){
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmssSSS");
Date date = new Date();
File file = new File(this.context.repair.buildDirectory + "/patches", "Candidate" + candidate + "_Generation" + generation + "_" + dateFormat.format(date));
BufferedWriter out = null;
/* Store the .class files for the program so we can verify. */
this.context.compiler.storeCompiled(this.context.repair.buildDirectory + "/classes_Candidate" + candidate + "_Generation" + generation + "_" + dateFormat.format(date));
/* Log the mutation events that produced the patch. */
try{
file.createNewFile();
out = new BufferedWriter(new FileWriter(file));
for(String s : this.patches){
out.write(s);
}
} catch(Exception e) { }
try{
if(out != null) out.close();
} catch(Exception ignore) { System.out.println("Problem closing writer for " + file.getName() + "."); }
}
/**
* Temp method for debugging.
* @throws Exception
*/
public void logMutation(Mutation m, int candidate, int generation) throws Exception{
Utilities.writeToFileAppend(new File(this.context.repair.buildDirectory + "/mutation-log"),
("Candidate " + candidate + ", Generation" + generation
+ "\n" + m.toString()).getBytes());
}
public void logCompileError(int candidate, int generation, String message) throws Exception {
Utilities.writeToFileAppend(new File(this.context.repair.buildDirectory + "/compile-log"),
("Candidate " + candidate + ", Generation" + generation
+ "\n" + message + "\n********************\n").getBytes());
}
}