package spoon.test.api;
import org.apache.commons.io.FileUtils;
import org.junit.Assert;
import org.junit.Test;
import spoon.Launcher;
import spoon.SpoonAPI;
import spoon.compiler.Environment;
import spoon.reflect.code.CtIf;
import spoon.reflect.code.CtStatement;
import spoon.reflect.code.CtVariableAccess;
import spoon.reflect.declaration.CtClass;
import spoon.reflect.declaration.CtElement;
import spoon.reflect.declaration.CtMethod;
import spoon.reflect.declaration.CtNamedElement;
import spoon.reflect.declaration.CtPackage;
import spoon.reflect.declaration.CtParameter;
import spoon.reflect.declaration.CtType;
import spoon.reflect.factory.Factory;
import spoon.reflect.reference.CtTypeReference;
import spoon.reflect.visitor.CtVisitor;
import spoon.reflect.visitor.Query;
import spoon.reflect.visitor.filter.AbstractFilter;
import spoon.reflect.visitor.filter.TypeFilter;
import spoon.support.DerivedProperty;
import spoon.support.JavaOutputProcessor;
import spoon.support.UnsettableProperty;
import spoon.support.reflect.declaration.CtElementImpl;
import spoon.template.Local;
import spoon.template.TemplateMatcher;
import spoon.template.TemplateParameter;
import spoon.test.api.testclasses.Bar;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public class APITest {
@Test
public void testBasicAPIUsage() throws Exception {
// this test shows a basic usage of the Launcher API without command line
// and asserts there is no exception
Launcher spoon = new Launcher();
spoon.setArgs(new String[] {"--compile", "--output-type", "compilationunits" });
spoon.addInputResource("src/test/resources/spoon/test/api");
spoon.run();
Factory factory = spoon.getFactory();
for (CtPackage p : factory.Package().getAll()) {
spoon.getEnvironment().debugMessage("package: " + p.getQualifiedName());
}
for (CtType<?> s : factory.Class().getAll()) {
spoon.getEnvironment().debugMessage("class: "+s.getQualifiedName());
}
}
@Test
public void testOverrideOutputWriter() throws Exception {
// this test that we can correctly set the Java output processor
final List<Object> l = new ArrayList<Object>();
Launcher spoon = new Launcher() {
@Override
public JavaOutputProcessor createOutputWriter(File sourceOutputDir, Environment environment) {
return new JavaOutputProcessor() {
@Override
public void process(CtNamedElement e) {
l.add(e);
}
@Override
public void init() {
// we do nothing
}
};
}
};
spoon.run(new String[] {
"-i", "src/test/resources/spoon/test/api/",
"-o", "fancy/fake/apitest" // we shouldn't write anything anyway
});
Assert.assertEquals(2, l.size());
}
@Test
public void testDuplicateEntry() throws Exception {
// it's possible to pass twice the same file as parameter
// the virtual folder removes the duplicate before passing to JDT
try {
String duplicateEntry = "src/test/resources/spoon/test/api/Foo.java";
// check on the JDK API
// this is later use by FileSystemFile
assertTrue(new File(duplicateEntry).getCanonicalFile().equals(new File("./"+duplicateEntry).getCanonicalFile()));
Launcher.main(new String[] {
"-i",
// note the nasty ./
duplicateEntry + File.pathSeparator + "./" + duplicateEntry,
"-o", "target/spooned/apitest" });
} catch (IllegalArgumentException e) // from JDT
{
fail();
}
}
@Test
public void testDuplicateFolder() throws Exception {
// it's possible to pass twice the same folder as parameter
// the virtual folder removes the duplicate before passing to JDT
try {
String duplicateEntry = "src/test/resources/spoon/test/api/";
Launcher.main(new String[] {
"-i",
duplicateEntry + File.pathSeparator + "./" + duplicateEntry,
"-o", "target/spooned/apitest" });
} catch (IllegalArgumentException e) // from JDT
{
fail();
}
}
@Test
public void testDuplicateFilePlusFolder() throws Exception {
// more complex case: a file is given, together with the enclosing folder
try {
Launcher.main(new String[] {
"-i",
"src/test/resources/spoon/test/api/" + File.pathSeparator
+ "src/test/resources/spoon/test/api/Foo.java",
"-o", "target/spooned/apitest" });
} catch (IllegalArgumentException e) // from JDT
{
fail();
}
}
@Test(expected=Exception.class)
public void testNotValidInput() throws Exception {
String invalidEntry = "does/not/exists//Foo.java";
Launcher.main(new String[] { "-i",
invalidEntry,
"-o",
"target/spooned/apitest" });
}
@Test
public void testAddProcessorMethodInSpoonAPI() throws Exception {
final SpoonAPI launcher = new Launcher();
launcher.addInputResource("./src/test/java/spoon/test/api/testclasses");
launcher.setSourceOutputDirectory("./target/spooned");
final AwesomeProcessor processor = new AwesomeProcessor();
launcher.addProcessor(processor);
launcher.run();
assertEquals(1, processor.getElements().size());
final CtClass<Bar> actual = processor.getElements().get(0);
assertEquals(2, actual.getMethods().size());
assertNotNull(actual.getMethodsByName("prepareMojito").get(0));
assertNotNull(actual.getMethodsByName("makeMojito").get(0));
}
@Test
public void testOutputOfSpoon() throws Exception {
final File sourceOutput = new File("./target/spoon/test/output/");
final SpoonAPI launcher = new Launcher();
launcher.addInputResource("./src/test/java/spoon/test/api/testclasses");
launcher.setSourceOutputDirectory(sourceOutput);
launcher.run();
assertTrue(sourceOutput.exists());
}
@Test
public void testDestinationOfSpoon() throws Exception {
final File binaryOutput = new File("./target/spoon/test/binary/");
final Launcher launcher = new Launcher();
launcher.getEnvironment().setShouldCompile(true);
launcher.addInputResource("./src/test/java/spoon/test/api/testclasses");
launcher.setSourceOutputDirectory("./target/spooned");
launcher.setBinaryOutputDirectory(binaryOutput);
launcher.run();
assertTrue(binaryOutput.exists());
}
@Test
public void testPrintNotAllSourcesWithFilter() throws Exception {
final File target = new File("./target/print-not-all/default");
final SpoonAPI launcher = new Launcher();
launcher.getEnvironment().setNoClasspath(true);
launcher.addInputResource("./src/main/java");
launcher.setSourceOutputDirectory(target);
launcher.setOutputFilter(new AbstractFilter<CtType<?>>(CtType.class) {
@Override
public boolean matches(CtType<?> element) {
return "spoon.Launcher".equals(element.getQualifiedName())
|| "spoon.template.AbstractTemplate".equals(element.getQualifiedName());
}
});
launcher.run();
List<File> list = new ArrayList<>(FileUtils.listFiles(target, new String[] {"java"}, true));
final List<String> filesName = list.stream().map(File::getName).sorted().collect(Collectors.<String>toList());
assertEquals(2, filesName.size());
assertEquals("AbstractTemplate.java", filesName.get(0));
assertEquals("Launcher.java", filesName.get(1));
}
@Test
public void testPrintNotAllSourcesWithNames() throws Exception {
final File target = new File("./target/print-not-all/array");
final SpoonAPI launcher = new Launcher();
launcher.getEnvironment().setNoClasspath(true);
launcher.addInputResource("./src/main/java");
launcher.setSourceOutputDirectory(target);
launcher.setOutputFilter("spoon.Launcher", "spoon.template.AbstractTemplate");
launcher.run();
List<File> list = new ArrayList<>(FileUtils.listFiles(target, new String[] {"java"}, true));
final List<String> filesName = list.stream().map(File::getName).sorted().collect(Collectors.<String>toList());
assertEquals(2, filesName.size());
assertEquals("AbstractTemplate.java", filesName.get(0));
assertEquals("Launcher.java", filesName.get(1));
}
@Test
public void testPrintNotAllSourcesInCommandLine() throws Exception {
final File target = new File("./target/print-not-all/command");
final SpoonAPI launcher = new Launcher();
launcher.run(new String[] {
"-i", "./src/main/java", //
"-o", "./target/print-not-all/command", //
"-f", "spoon.Launcher:spoon.template.AbstractTemplate", //
"--noclasspath"
});
List<File> list = new ArrayList<>(FileUtils.listFiles(target, new String[] {"java"}, true));
final List<String> filesName = list.stream().map(File::getName).sorted().collect(Collectors.<String>toList());
assertEquals(2, filesName.size());
assertEquals("AbstractTemplate.java", filesName.get(0));
assertEquals("Launcher.java", filesName.get(1));
}
@Test
public void testInvalidateCacheOfCompiler() throws Exception {
final Launcher spoon = new Launcher();
spoon.addInputResource("./src/test/java/spoon/test/api/testclasses/Bar.java");
spoon.setSourceOutputDirectory("./target/api");
spoon.getEnvironment().setNoClasspath(true);
spoon.run();
assertTrue(spoon.getModelBuilder().compile());
final CtClass<Bar> aClass = spoon.getFactory().Class().get(Bar.class);
final CtMethod aMethod = spoon.getFactory().Core().createMethod();
aMethod.setSimpleName("foo");
aMethod.setType(spoon.getFactory().Type().BOOLEAN_PRIMITIVE);
aMethod.setBody(spoon.getFactory().Core().createBlock());
aClass.addMethod(aMethod);
assertFalse(spoon.getModelBuilder().compile());
aClass.removeMethod(aMethod);
assertTrue(spoon.getModelBuilder().compile());
}
@Test
public void testSetterInNodes() throws Exception {
// contract: Check that all setters of an object have a condition to check
// that the new value is != null to avoid NPE when we set the parent.
class SetterMethodWithoutCollectionsFilter extends TypeFilter<CtMethod<?>> {
private final List<CtTypeReference<?>> collections = new ArrayList<>(4);
public SetterMethodWithoutCollectionsFilter(Factory factory) {
super(CtMethod.class);
for (Class<?> aCollectionClass : Arrays.asList(Collection.class, List.class, Map.class, Set.class)) {
collections.add(factory.Type().createReference(aCollectionClass));
}
}
@Override
public boolean matches(CtMethod<?> element) {
boolean isSetter = isSetterMethod(element);
boolean isNotSubType = !isSubTypeOfCollection(element);
// setter with unsettableProperty should not respect the contract, as well as derived properties
boolean doesNotHaveUnsettableAnnotation = doesNotHaveUnsettableAnnotation(element);
boolean isNotSetterForADerivedProperty = isNotSetterForADerivedProperty(element);
boolean superMatch = super.matches(element);
return isSetter && doesNotHaveUnsettableAnnotation && isNotSetterForADerivedProperty && isNotSubType && superMatch;
}
private boolean isNotSetterForADerivedProperty(CtMethod<?> method) {
String methodName = method.getSimpleName();
String getterName = methodName.replace("set","get");
if (getterName.equals(methodName)) {
return false;
}
CtClass<?> zeClass = (CtClass)method.getParent();
List<CtMethod<?>> getterMethods = zeClass.getMethodsByName(getterName);
if (getterMethods.size() != 1) {
return false;
}
CtMethod<?> getterMethod = getterMethods.get(0);
return (getterMethod.getAnnotation(DerivedProperty.class) == null);
}
private boolean doesNotHaveUnsettableAnnotation(CtMethod<?> element) {
return (element.getAnnotation(UnsettableProperty.class) == null);
}
private boolean isSubTypeOfCollection(CtMethod<?> element) {
final List<CtParameter<?>> parameters = element.getParameters();
if (parameters.size() != 1) {
return false;
}
final CtTypeReference<?> type = parameters.get(0).getType();
for (CtTypeReference<?> aCollectionRef : collections) {
if (type.isSubtypeOf(aCollectionRef) || type.equals(aCollectionRef)) {
return true;
}
}
return false;
}
private boolean isSetterMethod(CtMethod<?> element) {
final List<CtParameter<?>> parameters = element.getParameters();
if (parameters.size() != 1) {
return false;
}
final CtTypeReference<?> typeParameter = parameters.get(0).getType();
final CtTypeReference<CtElement> ctElementRef = element.getFactory().Type().createReference(CtElement.class);
// isSubtypeOf will return true in case of equality
boolean isSubtypeof = typeParameter.isSubtypeOf(ctElementRef);
if (!isSubtypeof) {
return false;
}
return element.getSimpleName().startsWith("set") && element.getDeclaringType().getSimpleName().startsWith("Ct") && element.getBody() != null;
}
}
class CheckNotNullToSetParentMatcher extends CtElementImpl {
public TemplateParameter<CtVariableAccess<?>> _parameter_access_;
public void matcher() {
if (_parameter_access_.S() != null) {
_parameter_access_.S().setParent(this);
}
}
@Override
@Local
public void accept(CtVisitor visitor) {
}
}
final Launcher launcher = new Launcher();
launcher.setArgs(new String[] {"--output-type", "nooutput" });
launcher.getEnvironment().setNoClasspath(true);
// Implementations
launcher.addInputResource("./src/main/java/spoon/support/reflect/code");
launcher.addInputResource("./src/main/java/spoon/support/reflect/declaration");
launcher.addInputResource("./src/main/java/spoon/support/reflect/reference");
launcher.addInputResource("./src/test/java/" + this.getClass().getCanonicalName().replace(".", "/") + ".java");
// Needed for #isSubTypeOf method.
launcher.addInputResource("./src/main/java/spoon/reflect/");
launcher.buildModel();
// Template matcher.
CtClass<CheckNotNullToSetParentMatcher> matcherCtClass = launcher.getFactory().Class().get(CheckNotNullToSetParentMatcher.class);
CtIf templateRoot = matcherCtClass.getMethod("matcher").getBody().getStatement(0);
final List<CtMethod<?>> setters = Query.getElements(launcher.getFactory(), new SetterMethodWithoutCollectionsFilter(launcher.getFactory()));
assertTrue("Number of setters found null", setters.size() > 0);
for (CtStatement statement : setters.stream().map((Function<CtMethod<?>, CtStatement>) ctMethod -> ctMethod.getBody().getStatement(0)).collect(Collectors.toList())) {
// First statement should be a condition to protect the setter of the parent.
assertTrue("Check the method " + statement.getParent(CtMethod.class).getSignature() + " in the declaring class " + statement.getParent(CtType.class).getQualifiedName(), statement instanceof CtIf);
CtIf ifCondition = (CtIf) statement;
TemplateMatcher matcher = new TemplateMatcher(templateRoot);
assertEquals("Check the number of if in method " + statement.getParent(CtMethod.class).getSignature() + " in the declaring class " + statement.getParent(CtType.class).getQualifiedName(),1, matcher.find(ifCondition).size());
}
}
}