package spoon.test.refactoring;
import static org.junit.Assert.*;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import org.junit.Test;
import spoon.Launcher;
import spoon.OutputType;
import spoon.SpoonException;
import spoon.SpoonModelBuilder;
import spoon.refactoring.CtRenameLocalVariableRefactoring;
import spoon.reflect.code.CtLocalVariable;
import spoon.reflect.declaration.CtAnnotation;
import spoon.reflect.declaration.CtClass;
import spoon.reflect.declaration.CtElement;
import spoon.reflect.declaration.CtMethod;
import spoon.reflect.declaration.CtType;
import spoon.reflect.declaration.CtVariable;
import spoon.reflect.reference.CtTypeReference;
import spoon.test.refactoring.testclasses.TestTryRename;
import spoon.test.refactoring.testclasses.CtRenameLocalVariableRefactoringTestSubject;
import spoon.testing.utils.ModelUtils;
public class CtRenameLocalVariableRefactoringTest
{
@Test
public void testModelConsistency() throws Throwable {
//contract: check that all assertions in all methods of the RenameLocalVariableRefactorTestSubject are correct
new CtRenameLocalVariableRefactoringTestSubject().checkModelConsistency();
}
/**
* If you need to debug behavior of refactoring on the exact method and variable in the {@link CtRenameLocalVariableRefactoringTestSubject} model,
* then provide
* 1) name of method of {@link CtRenameLocalVariableRefactoringTestSubject}
* 2) original name variable in the method
* 3) new name of variable in the method
* then put breakpoint on the line `this.getClass();` below and the debugger stops just before
* the to be inspected refactoring starts
*/
private String[] DEBUG = new String[]{/*"nestedClassMethodWithoutRefs", "var3", "var1"*/};
/**
* The {@link CtRenameLocalVariableRefactoringTestSubject} class is loaded as spoon model. Then:
* - It looks for each CtVariable and it's CtAnnotation and tries to rename that variable to the name defined by annotation.
* - If the annotation name is prefixed with "-", then that refactoring should fail.
* - If the annotation name is not prefixed, then that refactoring should pass.
* If it behaves different then expected, then this test fails
*/
@Test
public void testRenameAllLocalVariablesOfRenameTestSubject() throws Exception {
CtClass<?> varRenameClass = (CtClass<?>)ModelUtils.buildClass(CtRenameLocalVariableRefactoringTestSubject.class);
CtTypeReference<TestTryRename> tryRename = varRenameClass.getFactory().createCtTypeReference(TestTryRename.class);
varRenameClass.getMethods().forEach(method->{
//debugging support
if(DEBUG.length==3 && DEBUG[0].equals(method.getSimpleName())==false) return;
method.filterChildren((CtVariable var)->true)
.map((CtVariable var)->var.getAnnotation(tryRename))
.forEach((CtAnnotation<TestTryRename> annotation)->{
String[] newNames = annotation.getActualAnnotation().value();
CtVariable<?> targetVariable = (CtVariable<?>)annotation.getAnnotatedElement();
for (String newName : newNames) {
boolean renameShouldPass = newName.startsWith("-")==false;
if (!renameShouldPass) {
newName = newName.substring(1);
}
if (targetVariable instanceof CtLocalVariable<?>) {
//debugging support
if(DEBUG.length==3 && DEBUG[1].equals(targetVariable.getSimpleName()) && DEBUG[2].equals(newName)) {
//put breakpoint here and continue debugging of the buggy case
this.getClass();
}
checkLocalVariableRename((CtLocalVariable<?>) targetVariable, newName, renameShouldPass);
} else {
//TODO test rename of other variables, e.g. parameters and catch... later
}
}
});
});
}
protected void checkLocalVariableRename(CtLocalVariable<?> targetVariable, String newName, boolean renameShouldPass) {
String originName = targetVariable.getSimpleName();
CtRenameLocalVariableRefactoring refactor = new CtRenameLocalVariableRefactoring();
refactor.setTarget(targetVariable);
refactor.setNewName(newName);
if(renameShouldPass) {
try {
refactor.refactor();
} catch(SpoonException e) {
throw new AssertionError(getParentMethodName(targetVariable)+" Rename of \""+originName+"\" should NOT fail when trying rename to \""+newName+"\"\n"+targetVariable.toString(), e);
}
assertEquals(getParentMethodName(targetVariable)+" Rename of \""+originName+"\" to \""+newName+"\" passed, but the name of variable was not changed", newName, targetVariable.getSimpleName());
assertCorrectModel(getParentMethodName(targetVariable)+" Rename of \""+originName+"\" to \""+newName+"\"");
} else {
try {
refactor.refactor();
fail(getParentMethodName(targetVariable)+" Rename of \""+originName+"\" should fail when trying rename to \""+newName+"\"");
} catch(SpoonException e) {
}
assertEquals(getParentMethodName(targetVariable)+" Rename of \""+originName+"\" failed when trying rename to \""+newName+"\" but the name of variable should not be changed", originName, targetVariable.getSimpleName());
}
if(renameShouldPass) {
rollback(targetVariable, originName);
}
assertEquals(originName, targetVariable.getSimpleName());
}
private void rollback(CtLocalVariable<?> targetVariable, String originName) {
String newName = targetVariable.getSimpleName();
CtRenameLocalVariableRefactoring refactor = new CtRenameLocalVariableRefactoring();
refactor.setTarget(targetVariable);
//rollback changes
refactor.setNewName(originName);
try {
refactor.refactor();
} catch(SpoonException e) {
throw new AssertionError(getParentMethodName(targetVariable)+" Rename of \""+originName+"\" to \""+newName+"\" passed, but rename back to \""+originName+"\" failed", e);
}
}
private void assertCorrectModel(String refactoringDescription) {
Launcher launcher = new Launcher();
File outputBinDirectory = new File("./target/spooned-refactoring-test");
if (!outputBinDirectory.exists()) {
outputBinDirectory.mkdirs();
}
launcher.setBinaryOutputDirectory(outputBinDirectory);
launcher.setSourceOutputDirectory(outputBinDirectory);
// 1) print modified model,
try {
launcher.getModelBuilder().generateProcessedSourceFiles(OutputType.CLASSES);
} catch (Throwable e) {
new AssertionError("The printing of java sources failed after: "+refactoringDescription, e);
}
// 2) build it
try {
// launcher.getModelBuilder().compile(SpoonModelBuilder.InputType.FILES);
launcher.getModelBuilder().compile(SpoonModelBuilder.InputType.CTTYPES);
} catch (Throwable e) {
new AssertionError("The compilation of java sources in "+launcher.getEnvironment().getBinaryOutputDirectory()+" failed after: "+refactoringDescription, e);
}
// 3) create instance using that new model and test consistency
try {
// varRenameClass.newInstance();
TestClassloader classLoader = new TestClassloader(launcher);
Class testModelClass = classLoader.loadClass(CtRenameLocalVariableRefactoringTestSubject.class.getName());
testModelClass.getMethod("checkModelConsistency").invoke(testModelClass.newInstance());
} catch (InvocationTargetException e) {
throw new AssertionError("The model validation of code in "+launcher.getEnvironment().getBinaryOutputDirectory()+" failed after: "+refactoringDescription, e.getTargetException());
} catch (Throwable e) {
throw new AssertionError("The model validation of code in "+launcher.getEnvironment().getBinaryOutputDirectory()+" failed after: "+refactoringDescription, e);
}
}
private class TestClassloader extends URLClassLoader {
TestClassloader(Launcher launcher) throws MalformedURLException {
super(new URL[] { new File(launcher.getEnvironment().getBinaryOutputDirectory()).toURL()}, CtRenameLocalVariableRefactoringTest.class.getClassLoader());
}
@Override
public Class<?> loadClass(String s) throws ClassNotFoundException {
try {
return findClass(s);
} catch (Exception e) {
return super.loadClass(s);
}
}
}
private String getParentMethodName(CtElement ele) {
CtMethod parentMethod = ele.getParent(CtMethod.class);
CtMethod m;
while(parentMethod!=null && (m=parentMethod.getParent(CtMethod.class))!=null) {
parentMethod = m;
}
if(parentMethod!=null) {
return parentMethod.getParent(CtType.class).getSimpleName()+"#"+parentMethod.getSimpleName();
} else {
return ele.getParent(CtType.class).getSimpleName()+"#annonymous block";
}
}
@Test
public void testRefactorWrongUsage() throws Exception {
CtType varRenameClass = ModelUtils.buildClass(CtRenameLocalVariableRefactoringTestSubject.class);
CtLocalVariable<?> local1Var = varRenameClass.filterChildren((CtLocalVariable<?> var)->var.getSimpleName().equals("local1")).first();
//contract: a target variable is not defined. Throw SpoonException
CtRenameLocalVariableRefactoring refactor = new CtRenameLocalVariableRefactoring();
refactor.setNewName("local1");
try {
refactor.refactor();
fail();
} catch(SpoonException e) {
//should fail - OK
}
//contract: invalid rename request to empty string. Throw SpoonException
refactor.setTarget(local1Var);
try {
refactor.setNewName("");
fail();
} catch(SpoonException e) {
//should fail - OK
}
//contract: invalid rename request to variable name which contains space. Throw SpoonException
try {
refactor.setNewName("x ");
fail();
} catch(SpoonException e) {
//should fail - OK
}
//contract: invalid rename request to variable name which contains space. Throw SpoonException
try {
refactor.setNewName("x y");
fail();
} catch(SpoonException e) {
//should fail - OK
}
//contract: invalid rename request to variable name which contains character which is not allowed in variable name. Throw SpoonException
try {
refactor.setNewName("x(");
fail();
} catch(SpoonException e) {
//should fail - OK
}
}
@Test
public void testRenameLocalVariableToSameName() throws Exception {
CtType varRenameClass = ModelUtils.buildClass(CtRenameLocalVariableRefactoringTestSubject.class);
CtLocalVariable<?> local1Var = varRenameClass.filterChildren((CtLocalVariable<?> var)->var.getSimpleName().equals("local1")).first();
CtRenameLocalVariableRefactoring refactor = new CtRenameLocalVariableRefactoring();
refactor.setTarget(local1Var);
refactor.setNewName("local1");
refactor.refactor();
assertEquals("local1", local1Var.getSimpleName());
}
}