package com.sora.util.akatsuki;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.junit.Assert;
import org.mockito.ArgumentCaptor;
import android.os.Bundle;
import com.google.common.collect.Sets;
public class RetainedStateTestEnvironment extends BaseTestEnvironment {
public enum Accessor {
PUT, GET
}
public interface FieldFilter {
boolean test(TestField field, Class<?> type, Type[] arguments);
}
private BundleRetainer<Object> retainer; // no <?> because mokito blows up
private BundleRetainerTester tester;
public RetainedStateTestEnvironment(IntegrationTestBase base, List<TestSource> sources) {
super(base, sources);
}
public RetainedStateTestEnvironment(IntegrationTestBase base, TestSource source,
TestSource... required) {
super(base, source, required);
}
@Override
protected void setupTestEnvironment() throws Exception {
final Class<?> testClass;
// the first class is our test class
final String fqcn = sources.get(0).fqcn();
System.out.println("Test environment contains:"
+ Arrays.toString(sources.stream().map(s -> s.className()).toArray()));
System.out.println("Loading " + fqcn + " as test class...");
testClass = classLoader().loadClass(fqcn);
RetainerCache retainerCache = null;
try {
final Class<?> retainerCacheClass = classLoader().loadClass(
Akatsuki.RETAINER_CACHE_PACKAGE + "." + Akatsuki.RETAINER_CACHE_NAME);
retainerCache = (RetainerCache) retainerCacheClass.newInstance();
} catch (Exception ignored) {
// doesn't really matter
// throw new AssertionError(ignored);
}
retainer = Internal.createRetainer(classLoader(), retainerCache, testClass, Retained.class);
tester = new BundleRetainerTester(this, testClass.newInstance(), mock(Bundle.class),
retainer);
}
public BundleRetainerTester tester() {
return tester;
}
public static class BundleRetainerTester {
private final TestEnvironment environment;
private final Object mockedSource;
private final Bundle mockedBundle;
private final BundleRetainer<Object> retainer;
BundleRetainerTester(TestEnvironment environment, Object mockedSource, Bundle mockedBundle,
BundleRetainer<Object> retainer) {
this.environment = environment;
this.mockedSource = mockedSource;
this.mockedBundle = mockedBundle;
this.retainer = retainer;
}
public void invokeSave() {
try {
retainer.save(mockedSource, mockedBundle);
} catch (Exception e) {
throw new AssertionError("Unable to invoke save. " + environment.printReport(), e);
}
}
public void invokeRestore() {
try {
retainer.restore(mockedSource, mockedBundle);
} catch (Exception e) {
throw new AssertionError("Unable to invoke restore. " + environment.printReport(),
e);
}
}
public void invokeSaveAndRestore() {
invokeSave();
invokeRestore();
}
public void testSaveRestoreInvocation(Predicate<String> namePredicate,
FieldFilter accessorTypeFilter, Set<RetainedTestField> fields,
Function<TestField, Integer> times) {
for (Accessor accessor : Accessor.values()) {
executeTestCaseWithFields(fields, namePredicate, accessorTypeFilter, accessor,
times);
}
}
public void testSaveRestoreInvocation(Predicate<String> namePredicate,
FieldFilter accessorTypeFilter, List<? extends RetainedTestField> fields,
Function<TestField, Integer> times) {
final HashSet<RetainedTestField> set = Sets.newHashSet(fields);
if (set.size() != fields.size())
throw new IllegalArgumentException("Duplicate fields are not allowed");
testSaveRestoreInvocation(namePredicate, accessorTypeFilter, set, times);
}
public static FieldFilter ALWAYS = (f, t, a) -> true;
public static FieldFilter NEVER = (f, t, a) -> false;
public static FieldFilter CLASS_EQ = (f, t, a) -> f.clazz.equals(t);
public static FieldFilter ASSIGNABLE = (f, t, a) -> t.isAssignableFrom(f.clazz);
public static class AccessorKeyPair {
public final String putKey;
public final String getKey;
public AccessorKeyPair(String putKey, String getKey) {
this.putKey = putKey;
this.getKey = getKey;
}
public void assertSameKeyUsed() {
Assert.assertEquals("Same key expected", putKey, getKey);
}
public void assertNotTheSame(AccessorKeyPair another) {
Assert.assertNotEquals("Different keys expected", putKey, another.putKey);
Assert.assertNotEquals("Different keys expected", getKey, another.getKey);
}
}
public AccessorKeyPair captureTestCaseKeysWithField(RetainedTestField field,
Predicate<String> methodNamePredicate, FieldFilter accessorTypeFilter) {
return new AccessorKeyPair(
captureTestCaseKeyWithField(field, methodNamePredicate, accessorTypeFilter,
Accessor.PUT),
captureTestCaseKeyWithField(field, methodNamePredicate, accessorTypeFilter,
Accessor.GET));
}
public String captureTestCaseKeyWithField(RetainedTestField field,
Predicate<String> methodNamePredicate, FieldFilter accessorTypeFilter,
Accessor accessor) {
final ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
for (Method method : Bundle.class.getMethods()) {
// check correct signature, name predicate and type
if (!checkMethodIsAccessor(method, accessor)
|| !methodNamePredicate.test(method.getName())
|| !filterTypes(method, accessor, accessorTypeFilter, field))
continue;
try {
if (accessor == Accessor.PUT) {
method.invoke(verify(mockedBundle, atLeastOnce()), captor.capture(),
any(field.clazz));
} else {
method.invoke(verify(mockedBundle, atLeastOnce()), captor.capture());
}
return captor.getValue();
} catch (Exception e) {
throw new AssertionError("Invocation of method " + method.getName()
+ " on mocked object " + "failed." + environment.printReport(), e);
}
}
throw new RuntimeException("No invocation caught for field: " + field.toString()
+ environment.printReport());
}
public void executeTestCaseWithFields(Set<? extends TestField> fieldList,
Predicate<String> methodNamePredicate, FieldFilter accessorTypeFilter,
Accessor accessor, Function<TestField, Integer> times) {
Set<TestField> allFields = new HashSet<>(fieldList);
for (Method method : Bundle.class.getMethods()) {
// check correct signature and name predicate
if (!checkMethodIsAccessor(method, accessor)
|| !methodNamePredicate.test(method.getName())) {
continue;
}
// find methods who's accessor type matches the given fields
List<TestField> matchingField = allFields.stream()
.filter(f -> filterTypes(method, accessor, accessorTypeFilter, f))
.collect(Collectors.toList());
// no signature match
if (matchingField.isEmpty()) {
continue;
}
// more than one match, we should have exactly one match
if (matchingField.size() > 1) {
throw new AssertionError(method.toString() + " matches multiple field "
+ fieldList + ", this is ambiguous and should not happen."
+ environment.printReport());
}
final TestField field = matchingField.get(0);
try {
if (accessor == Accessor.PUT) {
method.invoke(verify(mockedBundle, times(times.apply(field))),
eq(field.name), any(field.clazz));
} else {
method.invoke(verify(mockedBundle, times(times.apply(field))),
eq(field.name));
}
allFields.remove(field);
} catch (Exception e) {
throw new AssertionError("Invocation of method " + method.getName()
+ " on mocked object failed." + environment.printReport(), e);
}
}
if (!allFields.isEmpty())
throw new RuntimeException("While testing for accessor:" + accessor
+ " some fields are left untested because a suitable accessor cannot be found: "
+ allFields + environment.printReport());
}
private boolean filterTypes(Method method, Accessor accessor,
FieldFilter accessorTypeFilter, TestField field) {
Parameter[] parameters = method.getParameters();
Class<?> type = accessor == Accessor.PUT ? parameters[1].getType()
: method.getReturnType();
Type[] arguments = {};
final Type genericType = accessor == Accessor.PUT ? parameters[1].getParameterizedType()
: method.getGenericReturnType();
if (genericType instanceof ParameterizedType) {
// if field is not generic while accessor type is, bail
if (!field.generic()) {
return false;
}
// or else record the type argument for the filter
arguments = ((ParameterizedType) genericType).getActualTypeArguments();
}
return accessorTypeFilter.test(field, type, arguments);
}
private boolean checkMethodIsAccessor(Method method, Accessor accessor) {
// Bundle accessor format:
// put<Suffix>(String key, <Type>) : void
// get<Suffix>(String key) : <Type>
// the following are strictly obeyed
// first parameter will always be a string
// getter has 1 argument, setter has 2
// must start with "put" for getter ans "set" for setter
// <Suffix> cannot be empty
final String name = method.getName();
boolean correctSignature = name.startsWith(accessor.name().toLowerCase())
&& name.length() > accessor.name().length()
&& method.getParameterCount() == (accessor == Accessor.PUT ? 2 : 1);
if (!correctSignature)
return false;
final Parameter[] parameters = method.getParameters();
return parameters[0].getType().equals(String.class);
}
}
}