Skip to content

Commit

Permalink
Improve RandomSyntaxTestBase -> pull out MethodCallChain, focus on in…
Browse files Browse the repository at this point in the history
…terface types instead of resolving runtime types via TypeToken (necessary to support more extended generics for complex members syntax with inheritance; we need to pass information about actual type parameters of generic interface types on, instead of using TypeToken on runtime types which will in parts yield <? extends #99...> types instead of the correctly bounded declared type available via public API)

Issue: #38
Signed-off-by: Peter Gafert <peter.gafert@tngtech.com>
  • Loading branch information
codecholeric committed Mar 10, 2019
1 parent 72d3704 commit a7c9006
Show file tree
Hide file tree
Showing 46 changed files with 785 additions and 154 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.tngtech.archunit.testutil.syntax;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

import com.tngtech.archunit.base.Optional;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.tngtech.archunit.testutil.TestUtils.invoke;

class MethodCallChain {
private final MethodChoiceStrategy methodChoiceStrategy;
private Optional<Method> nextMethodCandidate;
private TypedValue currentValue;

MethodCallChain(MethodChoiceStrategy methodChoiceStrategy, TypedValue typedValue) {
this.methodChoiceStrategy = checkNotNull(methodChoiceStrategy);
currentValue = checkNotNull(typedValue);
nextMethodCandidate = methodChoiceStrategy.choose(typedValue.getType());
}

TypedValue getCurrentValue() {
return checkNotNull(currentValue);
}

Method getNextMethodCandidate() {
return nextMethodCandidate.get();
}

boolean hasAnotherMethodCandidate() {
return nextMethodCandidate.isPresent();
}

void invokeNextMethodCandidate(Parameters parameters) {
PropagatedType nextType = currentValue.resolveType(nextMethodCandidate.get().getGenericReturnType());
Object nextValue = invoke(nextMethodCandidate.get(), currentValue.getValue(), parameters.getValues());
currentValue = validate(new TypedValue(nextType, nextValue));
nextMethodCandidate = methodChoiceStrategy.choose(currentValue.getType());
}

private TypedValue validate(TypedValue value) {
checkArgument(Modifier.isPublic(value.getRawType().getModifiers()),
"Chosen type %s is not public", value.getRawType().getName());

checkArgument(!value.getRawType().equals(Object.class),
"Type of value got too generic: %s", value.getValue().getClass());

checkState(currentValue.getValue() != null,
"Invoking %s() on %s returned null (%s.java:0)",
nextMethodCandidate.get().getName(), value, value.getValue().getClass().getSimpleName());

return value;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package com.tngtech.archunit.testutil.syntax;

import java.util.Collections;

import com.tngtech.archunit.testutil.syntax.callchainexamples.fivestepswithgenericshierarchy.FiveStepsHierarchyImplementationStep1;
import com.tngtech.archunit.testutil.syntax.callchainexamples.fivestepswithgenericshierarchy.FiveStepsHierarchyImplementationStep2;
import com.tngtech.archunit.testutil.syntax.callchainexamples.fivestepswithgenericshierarchy.FiveStepsHierarchyImplementationStep3;
import com.tngtech.archunit.testutil.syntax.callchainexamples.fivestepswithgenericshierarchy.FiveStepsHierarchyImplementationStep4;
import com.tngtech.archunit.testutil.syntax.callchainexamples.fivestepswithgenericshierarchy.FiveStepsHierarchyImplementationStep5;
import com.tngtech.archunit.testutil.syntax.callchainexamples.fivestepswithgenericshierarchy.FiveStepsInterfaceChildStep1;
import com.tngtech.archunit.testutil.syntax.callchainexamples.fivestepswithgenericshierarchy.FiveStepsInterfaceChildStep2;
import com.tngtech.archunit.testutil.syntax.callchainexamples.fivestepswithgenericshierarchy.FiveStepsInterfaceChildStep3;
import com.tngtech.archunit.testutil.syntax.callchainexamples.fivestepswithgenericshierarchy.FiveStepsInterfaceChildStep5;
import com.tngtech.archunit.testutil.syntax.callchainexamples.fivestepswithgenericshierarchy.FiveStepsInterfaceParentStep4;
import com.tngtech.archunit.testutil.syntax.callchainexamples.fourstepswithgenerics.FourStepsImplementationStep1;
import com.tngtech.archunit.testutil.syntax.callchainexamples.fourstepswithgenerics.FourStepsImplementationStep2;
import com.tngtech.archunit.testutil.syntax.callchainexamples.fourstepswithgenerics.FourStepsImplementationStep3;
import com.tngtech.archunit.testutil.syntax.callchainexamples.fourstepswithgenerics.FourStepsImplementationStep4;
import com.tngtech.archunit.testutil.syntax.callchainexamples.fourstepswithgenerics.FourStepsInterfaceStep1;
import com.tngtech.archunit.testutil.syntax.callchainexamples.fourstepswithgenerics.FourStepsInterfaceStep2;
import com.tngtech.archunit.testutil.syntax.callchainexamples.fourstepswithgenerics.FourStepsInterfaceStep3;
import com.tngtech.archunit.testutil.syntax.callchainexamples.fourstepswithgenerics.FourStepsInterfaceStep4;
import com.tngtech.archunit.testutil.syntax.callchainexamples.longunboundedpropagation.FourStepsLongUnboundImplementationStep1;
import com.tngtech.archunit.testutil.syntax.callchainexamples.longunboundedpropagation.FourStepsLongUnboundImplementationStep4;
import com.tngtech.archunit.testutil.syntax.callchainexamples.longunboundedpropagation.FourStepsLongUnboundInterfaceStep1;
import com.tngtech.archunit.testutil.syntax.callchainexamples.longunboundedpropagation.FourStepsLongUnboundInterfaceStep4;
import com.tngtech.archunit.testutil.syntax.callchainexamples.threestepswithgenerics.ThreeStepsImplementationStep1;
import com.tngtech.archunit.testutil.syntax.callchainexamples.threestepswithgenerics.ThreeStepsImplementationStep2;
import com.tngtech.archunit.testutil.syntax.callchainexamples.threestepswithgenerics.ThreeStepsImplementationStep3;
import com.tngtech.archunit.testutil.syntax.callchainexamples.threestepswithgenerics.ThreeStepsInterfaceStep1;
import com.tngtech.archunit.testutil.syntax.callchainexamples.threestepswithgenerics.ThreeStepsInterfaceStep2;
import com.tngtech.archunit.testutil.syntax.callchainexamples.threestepswithgenerics.ThreeStepsInterfaceStep3;
import com.tngtech.java.junit.dataprovider.DataProvider;
import com.tngtech.java.junit.dataprovider.DataProviderRunner;
import com.tngtech.java.junit.dataprovider.UseDataProvider;
import org.junit.Test;
import org.junit.runner.RunWith;

import static com.tngtech.archunit.testutil.syntax.MethodChoiceStrategy.chooseAllArchUnitSyntaxMethods;
import static com.tngtech.java.junit.dataprovider.DataProviders.testForEach;
import static org.assertj.core.api.Assertions.assertThat;

@RunWith(DataProviderRunner.class)
public class MethodCallChainTest {
@DataProvider
public static Object[][] callChainTestCases() {
return testForEach(
CallChainTestCase
.start(ThreeStepsInterfaceStep1.class, new ThreeStepsImplementationStep1())
.numberOfInvocations(1)
.expect(ThreeStepsInterfaceStep2.class, ThreeStepsImplementationStep2.class),
CallChainTestCase
.start(ThreeStepsInterfaceStep1.class, new ThreeStepsImplementationStep1())
.numberOfInvocations(2)
.expect(ThreeStepsInterfaceStep3.class, ThreeStepsImplementationStep3.class),

CallChainTestCase
.start(FourStepsInterfaceStep1.class, new FourStepsImplementationStep1())
.numberOfInvocations(1)
.expect(FourStepsInterfaceStep2.class, FourStepsImplementationStep2.class),
CallChainTestCase
.start(FourStepsInterfaceStep1.class, new FourStepsImplementationStep1())
.numberOfInvocations(2)
.expect(FourStepsInterfaceStep3.class, FourStepsImplementationStep3.class),
CallChainTestCase
.start(FourStepsInterfaceStep1.class, new FourStepsImplementationStep1())
.numberOfInvocations(3)
.expect(FourStepsInterfaceStep4.class, FourStepsImplementationStep4.class),

CallChainTestCase
.start(FourStepsLongUnboundInterfaceStep1.class, new FourStepsLongUnboundImplementationStep1())
.numberOfInvocations(3)
.expect(FourStepsLongUnboundInterfaceStep4.class, FourStepsLongUnboundImplementationStep4.class),

CallChainTestCase
.start(FiveStepsInterfaceChildStep1.class, new FiveStepsHierarchyImplementationStep1())
.numberOfInvocations(1)
.expect(FiveStepsInterfaceChildStep2.class, FiveStepsHierarchyImplementationStep2.class),
CallChainTestCase
.start(FiveStepsInterfaceChildStep1.class, new FiveStepsHierarchyImplementationStep1())
.numberOfInvocations(2)
.expect(FiveStepsInterfaceChildStep3.class, FiveStepsHierarchyImplementationStep3.class),
CallChainTestCase
.start(FiveStepsInterfaceChildStep1.class, new FiveStepsHierarchyImplementationStep1())
.numberOfInvocations(3)
// Fall through to parent, nevertheless four invocations bring us back to the interface subtype
.expect(FiveStepsInterfaceParentStep4.class, FiveStepsHierarchyImplementationStep4.class),
CallChainTestCase
.start(FiveStepsInterfaceChildStep1.class, new FiveStepsHierarchyImplementationStep1())
.numberOfInvocations(4)
.expect(FiveStepsInterfaceChildStep5.class, FiveStepsHierarchyImplementationStep5.class)
);
}

@Test
@UseDataProvider("callChainTestCases")
public <T> void run_test_cases(CallChainTestCase<T> testCase) {
MethodCallChain callChain = createCallChainStart(testCase.startInterface, testCase.startImplementation);

for (int i = 0; i < testCase.numberOfInvocations; i++) {
invokeNext(callChain);
}

checkResult(callChain.getCurrentValue(), testCase.expectedInterface, testCase.expectedImplementationType);
}

private void checkResult(TypedValue currentValue, Class<?> expectedInterface, Class<?> expectedImplementation) {
assertThat(currentValue.getType().getRawType()).as("Interface type").isEqualTo(expectedInterface);
assertThat(currentValue.getValue()).as("current value").isInstanceOf(expectedImplementation);
}

private <T> MethodCallChain createCallChainStart(Class<T> startInterface, T startImplementation) {
PropagatedType type = new PropagatedType(startInterface);
return new MethodCallChain(chooseAllArchUnitSyntaxMethods(), new TypedValue(type, startImplementation));
}

private void invokeNext(MethodCallChain callChain) {
callChain.invokeNextMethodCandidate(new Parameters("FIXME", Collections.<Parameter>emptyList()));
}

private static class CallChainTestCase<T> {
private final Class<T> startInterface;
private final T startImplementation;
private int numberOfInvocations;
private Class<?> expectedInterface;
private Class<?> expectedImplementationType;

CallChainTestCase(Class<T> startInterface, T startImplementation) {

this.startInterface = startInterface;
this.startImplementation = startImplementation;
}

static <T> CallChainTestCase<T> start(Class<T> startInterface, T startImplementation) {
return new CallChainTestCase<>(startInterface, startImplementation);
}

CallChainTestCase<T> numberOfInvocations(int number) {
this.numberOfInvocations = number;
return this;
}

<V> CallChainTestCase<T> expect(Class<V> expectedInterface, Class<? extends V> expectedImplementationType) {
this.expectedInterface = expectedInterface;
this.expectedImplementationType = expectedImplementationType;
return this;
}

@Override
public String toString() {
return String.format("%s{start: [%s, %s], numberOfInvocations: [%d], expectedResult: [%s, %s]}",
getClass().getSimpleName(),
startInterface.getSimpleName(), startImplementation.getClass().getSimpleName(),
numberOfInvocations,
expectedInterface.getSimpleName(), expectedImplementationType.getSimpleName());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,20 @@

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Random;

import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.reflect.TypeToken;
import com.google.common.collect.ImmutableList;
import com.tngtech.archunit.base.Optional;
import com.tngtech.archunit.lang.ArchRule;

import static com.google.common.base.MoreObjects.toStringHelper;
import static com.google.common.base.Predicates.or;

public class MethodChoiceStrategy {
Expand Down Expand Up @@ -43,23 +48,39 @@ public boolean apply(Method input) {
};
}

Optional<Method> choose(TypeToken<?> typeToken) {
List<Method> methods = getPossibleMethodCandidates(typeToken.getRawType());
Optional<Method> choose(PropagatedType type) {
List<Method> methods = getPossibleMethodCandidates(type.getRawType());
return !methods.isEmpty()
? Optional.of(methods.get(random.nextInt(methods.size())))
: Optional.<Method>absent();
}

private List<Method> getPossibleMethodCandidates(Class<?> clazz) {
List<Method> result = new ArrayList<>();
for (Method method : clazz.getMethods()) {
for (Method method : getInvocableMethods(clazz)) {
if (isCandidate(method)) {
result.add(method);
}
}
return result;
}

private Collection<Method> getInvocableMethods(Class<?> clazz) {
Map<MethodKey, Method> result = new HashMap<>();
for (Method method : clazz.getMethods()) {
MethodKey key = MethodKey.of(method);
Method invocableCandidate = result.containsKey(key)
? resolveMethodInMoreSpecificType(method, result.get(key))
: method;
result.put(key, invocableCandidate);
}
return result.values();
}

private Method resolveMethodInMoreSpecificType(Method first, Method second) {
return second.getDeclaringClass().isAssignableFrom(first.getDeclaringClass()) ? first : second;
}

private boolean isCandidate(Method method) {
return belongsToArchUnit(method) && isNoArchRuleMethod(method) && !ignorePredicate.apply(method);
}
Expand All @@ -80,4 +101,44 @@ private boolean methodDoesNotBelongTo(Class<?> type, Method method) {
return true;
}
}

private static class MethodKey {
private final String name;
private final List<Class<?>> parameterTypes;

private MethodKey(Method method) {
name = method.getName();
parameterTypes = ImmutableList.copyOf(method.getParameterTypes());
}

static MethodKey of(Method method) {
return new MethodKey(method);
}

@Override
public int hashCode() {
return Objects.hash(name, parameterTypes);
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
final MethodKey other = (MethodKey) obj;
return Objects.equals(this.name, other.name)
&& Objects.equals(this.parameterTypes, other.parameterTypes);
}

@Override
public String toString() {
return toStringHelper(this)
.add("name", name)
.add("parameterTypes", parameterTypes)
.toString();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.tngtech.archunit.testutil.syntax;

import static com.google.common.base.MoreObjects.toStringHelper;

class Parameter {
private final Object value;
private final String description;

Parameter(Object value, String description) {
this.value = value;
this.description = description;
}

public Object getValue() {
return value;
}

public String getDescription() {
return description;
}

@Override
public String toString() {
return toStringHelper(this)
.add("value", getValue())
.add("description", getDescription())
.toString();
}
}
Loading

0 comments on commit a7c9006

Please sign in to comment.