/*
* Copyright (C) 2014 RoboVM AB
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/gpl-2.0.html>.
*/
package org.robovm.compiler.plugin.annotation;
import static org.junit.Assert.*;
import java.io.File;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.util.Arrays;
import org.junit.BeforeClass;
import org.junit.Test;
import org.robovm.compiler.ModuleBuilder;
import org.robovm.compiler.clazz.Clazz;
import org.robovm.compiler.clazz.Dependency;
import org.robovm.compiler.config.Config;
import org.robovm.compiler.config.Config.Builder;
import org.robovm.compiler.config.FakeHome;
import org.robovm.rt.bro.annotation.Bridge;
/**
* Tests {@link AnnotationImplPlugin}.
*/
public class AnnotationImplPluginTest {
static Config config;
@BeforeClass
public static void initialize() throws IOException {
Builder builder = new Builder();
for (String p : System.getProperty("sun.boot.class.path").split(File.pathSeparator)) {
builder.addBootClasspathEntry(new File(p));
}
for (String p : System.getProperty("java.class.path").split(File.pathSeparator)) {
builder.addClasspathEntry(new File(p));
}
builder.home(new FakeHome());
builder.mainClass("Main");
File cacheDir = Files.createTempDirectory(AnnotationImplPlugin.class.getSimpleName()).toFile();
builder.cacheDir(cacheDir);
config = builder.build();
}
private Clazz toClazz(Class<?> cls) {
return config.getClazzes().load(cls.getName().replace('.', '/'));
}
public @interface Anno1 {}
@Retention(RetentionPolicy.SOURCE)
public @interface Anno2 {}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Anno3 {}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Anno4 {
boolean boolean1();
boolean boolean2() default true;
byte byte1();
byte byte2() default 100;
short short1();
short short2() default 23000;
char char1();
char char2() default 46000;
int int1();
int int2() default 2_000_000_000;
long long1();
long long2() default 20_000_000_000L;
float float1();
float float2() default 1234567.654321f;
double double1();
double double2() default 7654321.234567;
String string1();
String string2() default "string2default";
Class<?> class1();
Class<?> class2() default byte.class;
Bridge anno1();
Bridge anno2() default @Bridge(symbol = "foo", dynamic = true);
boolean[] booleans1();
boolean[] booleans2() default {true, false, true};
byte[] bytes1();
byte[] bytes2() default {100, 101, 102};
short[] shorts1();
short[] shorts2() default {23000, 23001, 23002};
char[] chars1();
char[] chars2() default {46000, 46001, 46002};
int[] ints1();
int[] ints2() default {2_000_000_000, 2_000_000_001, 2_000_000_002};
long[] longs1();
long[] longs2() default {20_000_000_000L, 20_000_000_001L, 20_000_000_002L};
float[] floats1();
float[] floats2() default {1234567.654321f, 2234567.654321f, 3234567.654321f};
double[] doubles1();
double[] doubles2() default {7654321.234567, 8654321.234567, 9654321.234567};
String[] strings1();
String[] strings2() default {"a", "b", "c"};
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Anno5 {
Anno1 anno1() default @Anno1;
Anno2 anno2() default @Anno2;
}
@Anno3
@Anno4(
boolean1 = false,
byte1 = 101,
short1 = 23001,
char1 = 46001,
int1 = 2_000_000_001,
long1 = 20_000_000_001L,
float1 = 2234567.654321f,
double1 = 8654321.234567,
string1 = "string1value",
class1 = Integer.class,
anno1 = @Bridge(symbol = "bar", dynamic = true),
booleans1 = {false, true},
bytes1 = {102, 101, 100},
shorts1 = {23002, 23001, 23000},
chars1 = {46002, 46001, 46000},
ints1 = {2_000_000_002, 2_000_000_001, 2_000_000_000},
longs1 = {20_000_000_002L, 20_000_000_001L, 20_000_000_000L},
floats1 = {3234567.654321f, 2234567.654321f, 1234567.654321f},
doubles1 = {9654321.234567, 8654321.234567, 7654321.234567},
strings1 = {"c", "b", "a"})
public static class AnnoHost {}
@Test
public void testNotVisibleAnno() throws Exception {
AnnotationImplPlugin plugin = new AnnotationImplPlugin();
Clazz clazz = toClazz(Anno1.class);
clazz.resetClazzInfo();
plugin.beforeClass(config, clazz, new ModuleBuilder());
assertFalse(clazz.getClazzInfo().getAllDependencies().isEmpty());
}
@Test
public void testCreateSingleton1() throws Exception {
final File implFile = createAnnoImpl(Anno3.class);
Class<?> implClass = loadClassFromFile(implFile, Anno3.class.getName() + "$Impl");
Anno3 impl1 = (Anno3) implClass.getMethod("$createSingleton").invoke(null);
Anno3 impl2 = (Anno3) implClass.getMethod("$createSingleton").invoke(null);
// $createSingleton() should always return the same instance
assertSame(impl1, impl2);
}
@Test
public void testCreateSingleton2() throws Exception {
final File implFile = createAnnoImpl(Anno4.class);
Class<?> implClass = loadClassFromFile(implFile, Anno4.class.getName() + "$Impl");
Anno4 impl1 = (Anno4) implClass.getMethod("$createSingleton").invoke(null);
Anno4 impl2 = (Anno4) implClass.getMethod("$createSingleton").invoke(null);
// $createSingleton() should always return the same instance
assertSame(impl1, impl2);
}
@Test
public void testCreateImplNoMembers() throws Exception {
final File implFile = createAnnoImpl(Anno3.class);
Class<?> implClass = loadClassFromFile(implFile, Anno3.class.getName() + "$Impl");
Anno3 impl1 = (Anno3) implClass.getMethod("$create").invoke(null);
assertSame(Anno3.class, impl1.annotationType());
// $create() should always return the same instance when there are no members
Anno3 impl2 = (Anno3) implClass.getMethod("$create").invoke(null);
assertSame(impl1, impl2);
// Make sure our generated Anno3 impl is compatible with Anno3 impl instances generated by the current JVM
Anno3 annotation = AnnoHost.class.getAnnotation(Anno3.class);
assertTrue(annotation.equals(impl1));
assertTrue(impl1.equals(annotation));
assertEquals(annotation.hashCode(), impl1.hashCode());
assertEquals(annotation.toString(), impl1.toString());
assertSame(annotation.annotationType(), impl1.annotationType());
}
@Test
public void testCompatibleWithCurrentJvm() throws Exception {
final File implFile = createAnnoImpl(Anno4.class);
Class<?> implClass = loadClassFromFile(implFile, Anno4.class.getName() + "$Impl");
Anno4 impl1 = (Anno4) implClass.getMethod("$create").invoke(null);
assertSame(Anno4.class, impl1.annotationType());
assertEquals(impl1, impl1);
// Make sure arrays are cloned before returned
assertNotSame(impl1.booleans2(), impl1.booleans2());
assertNotSame(impl1.bytes2(), impl1.bytes2());
assertNotSame(impl1.shorts2(), impl1.shorts2());
assertNotSame(impl1.chars2(), impl1.chars2());
assertNotSame(impl1.ints2(), impl1.ints2());
assertNotSame(impl1.longs2(), impl1.longs2());
assertNotSame(impl1.floats2(), impl1.floats2());
assertNotSame(impl1.doubles2(), impl1.doubles2());
assertNotSame(impl1.strings2(), impl1.strings2());
// $create() should return a new instance
Anno4 impl2 = (Anno4) implClass.getMethod("$create").invoke(null);
assertNotSame(impl1, impl2);
// impl1 and impl2 should be equal. This tests the fastEquals() method.
assertEquals(impl2, impl1);
// Make sure our generated Anno4 impl is compatible with Anno4 impl instances generated by the current JVM
Anno4 annotation = AnnoHost.class.getAnnotation(Anno4.class);
// At first the instance should not be equal since our anno instances haven't got any values set yet except defaults.
assertFalse(impl1.equals(annotation));
// And hash codes should not match
assertFalse(annotation.hashCode() == impl1.hashCode());
// Make sure default values are equal
assertEquals(annotation.boolean2(), impl1.boolean2());
assertEquals(annotation.byte2(), impl1.byte2());
assertEquals(annotation.short2(), impl1.short2());
assertEquals(annotation.char2(), impl1.char2());
assertEquals(annotation.int2(), impl1.int2());
assertEquals(annotation.long2(), impl1.long2());
assertEquals(annotation.float2(), impl1.float2(), 0.0f);
assertEquals(annotation.double2(), impl1.double2(), 0.0);
assertEquals(annotation.string2(), impl1.string2());
assertEquals(annotation.class2(), impl1.class2());
assertEquals(annotation.anno2(), impl1.anno2());
assertTrue(Arrays.equals(annotation.booleans2(), impl1.booleans2()));
assertArrayEquals(annotation.bytes2(), impl1.bytes2());
assertArrayEquals(annotation.shorts2(), impl1.shorts2());
assertArrayEquals(annotation.chars2(), impl1.chars2());
assertArrayEquals(annotation.ints2(), impl1.ints2());
assertArrayEquals(annotation.longs2(), impl1.longs2());
assertTrue(Arrays.equals(annotation.floats2(), impl1.floats2()));
assertTrue(Arrays.equals(annotation.doubles2(), impl1.doubles2()));
assertArrayEquals(annotation.strings2(), impl1.strings2());
// Set the values which have no defaults and compare
setAnnotationMemberValue(impl1, "boolean1", annotation.boolean1());
setAnnotationMemberValue(impl1, "byte1", annotation.byte1());
setAnnotationMemberValue(impl1, "short1", annotation.short1());
setAnnotationMemberValue(impl1, "char1", annotation.char1());
setAnnotationMemberValue(impl1, "int1", annotation.int1());
setAnnotationMemberValue(impl1, "long1", annotation.long1());
setAnnotationMemberValue(impl1, "float1", annotation.float1());
setAnnotationMemberValue(impl1, "double1", annotation.double1());
setAnnotationMemberValue(impl1, "string1", annotation.string1());
setAnnotationMemberValue(impl1, "class1", annotation.class1());
setAnnotationMemberValue(impl1, "anno1", annotation.anno1());
setAnnotationMemberValue(impl1, "booleans1", annotation.booleans1());
setAnnotationMemberValue(impl1, "bytes1", annotation.bytes1());
setAnnotationMemberValue(impl1, "shorts1", annotation.shorts1());
setAnnotationMemberValue(impl1, "chars1", annotation.chars1());
setAnnotationMemberValue(impl1, "ints1", annotation.ints1());
setAnnotationMemberValue(impl1, "longs1", annotation.longs1());
setAnnotationMemberValue(impl1, "floats1", annotation.floats1());
setAnnotationMemberValue(impl1, "doubles1", annotation.doubles1());
setAnnotationMemberValue(impl1, "strings1", annotation.strings1());
assertEquals(annotation.boolean1(), impl1.boolean1());
assertEquals(annotation.byte1(), impl1.byte1());
assertEquals(annotation.short1(), impl1.short1());
assertEquals(annotation.char1(), impl1.char1());
assertEquals(annotation.int1(), impl1.int1());
assertEquals(annotation.long1(), impl1.long1());
assertEquals(annotation.float1(), impl1.float1(), 0.0f);
assertEquals(annotation.double1(), impl1.double1(), 0.0);
assertEquals(annotation.string1(), impl1.string1());
assertEquals(annotation.class1(), impl1.class1());
assertEquals(annotation.anno1(), impl1.anno1());
assertTrue(Arrays.equals(annotation.booleans1(), impl1.booleans1()));
assertArrayEquals(annotation.bytes1(), impl1.bytes1());
assertArrayEquals(annotation.shorts1(), impl1.shorts1());
assertArrayEquals(annotation.chars1(), impl1.chars1());
assertArrayEquals(annotation.ints1(), impl1.ints1());
assertArrayEquals(annotation.longs1(), impl1.longs1());
assertTrue(Arrays.equals(annotation.floats1(), impl1.floats1()));
assertTrue(Arrays.equals(annotation.doubles1(), impl1.doubles1()));
assertArrayEquals(annotation.strings1(), impl1.strings1());
assertTrue(annotation.equals(impl1));
assertTrue(impl1.equals(annotation));
assertEquals(annotation.hashCode(), impl1.hashCode());
assertSame(annotation.annotationType(), impl1.annotationType());
// We cannot require that toString() returns the same string as the ref
// impl does. We lay out members as they occur in the annotation definition
// while the ref impl orders them differently. We can however check that the
// number of chars and the sum of all chars are the same.
assertEquals(annotation.toString().length(), impl1.toString().length());
assertEquals(sum(annotation.toString()), sum(impl1.toString()));
}
@Test
public void testNonRuntimeVisibleAnnotationsAsDefaultValues() throws Exception {
final File implFile = createAnnoImpl(Anno5.class);
Class<?> implClass = loadClassFromFile(implFile, Anno5.class.getName() + "$Impl");
Anno5 impl = (Anno5) implClass.getMethod("$create").invoke(null);
assertTrue(impl.anno1() instanceof Anno1);
assertTrue(impl.anno2() instanceof Anno2);
}
private int sum(String s) {
int sum = 0;
for (int i = 0; i < s.length(); i++) {
sum += s.charAt(i);
}
return sum;
}
private void setAnnotationMemberValue(Object anno, String memberName, Object value) throws Exception {
Field f = anno.getClass().getDeclaredField("m$" + memberName);
f.setAccessible(true);
f.set(anno, value);
}
private File createAnnoImpl(Class<? extends Annotation> annoClass) {
AnnotationImplPlugin plugin = new AnnotationImplPlugin();
Clazz clazz = toClazz(annoClass);
clazz.resetClazzInfo();
plugin.beforeClass(config, clazz, new ModuleBuilder());
assertFalse(clazz.getClazzInfo().getAllDependencies().isEmpty());
Dependency dep = clazz.getClazzInfo().getAllDependencies().iterator().next();
assertEquals(clazz.getInternalName() + "$Impl", dep.getClassName());
final File implFile = new File(config.getGeneratedClassDir(clazz.getPath()), clazz.getInternalName() + "$Impl.class");
assertTrue(implFile.exists());
return implFile;
}
private Class<?> loadClassFromFile(final File implFile, final String className) throws ClassNotFoundException {
Class<?> implClass = new ClassLoader(getClass().getClassLoader()) {
public Class<?> loadClass(String name) throws ClassNotFoundException {
if (name.equals(className)) {
try {
byte[] bytes = Files.readAllBytes(implFile.toPath());
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e) {
throw (ClassNotFoundException) new ClassNotFoundException(name).initCause(e);
}
} else {
return super.loadClass(name);
}
}
}.loadClass(className);
return implClass;
}
}