Skip to content

Commit

Permalink
replace random by strategic specimen-selection
Browse files Browse the repository at this point in the history
Instead of picking a random implementation candidate for an abstract
type at build-time of the specimen, all matching candidates are passed
to the specimen and iterated over at creation-time until a suitable
implementation is found. This allows to try multiple implementations and
to fall back to a proxy if none of the candidates can be manufactured.
  • Loading branch information
Nylle committed Nov 23, 2023
1 parent 3f612ea commit 1309586
Show file tree
Hide file tree
Showing 12 changed files with 491 additions and 287 deletions.
30 changes: 11 additions & 19 deletions src/main/java/com/github/nylle/javafixture/ClassPathScanner.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,15 @@

import java.lang.reflect.Type;
import java.util.List;
import java.util.Optional;
import java.util.Random;
import java.util.stream.Collectors;

public class ClassPathScanner {

public <T> Optional<SpecimenType<T>> findRandomClassFor(SpecimenType<T> type) {
public <T> List<SpecimenType<? extends T>> findAllClassesFor(SpecimenType<T> type) {
try (ScanResult scanResult = new ClassGraph().enableAllInfo().scan()) {

var result = filter(scanResult, type);

if (result.isEmpty()) {
return Optional.empty();
}

var implementingClass = result.get(new Random().nextInt(result.size()));

if (isNotParametrized(implementingClass)) {
return Optional.of(SpecimenType.fromClass(implementingClass.loadClass()));
}

return Optional.of(SpecimenType.fromRawType(implementingClass.loadClass(), resolveTypeArguments(type, implementingClass)));

return filter(scanResult, type).stream().map(x -> specimenTypeOf(x, type)).collect(Collectors.toList());
} catch (Exception ex) {
return Optional.empty();
return List.of();
}
}

Expand All @@ -53,6 +37,14 @@ private <T> List<ClassInfo> filter(ScanResult scanResult, SpecimenType<T> type)
return List.of();
}

private static <T, R extends T> SpecimenType<R> specimenTypeOf(ClassInfo implementingClass, SpecimenType<T> type) {
if (isNotParametrized(implementingClass)) {
return SpecimenType.fromClass(implementingClass.loadClass());
}

return SpecimenType.fromRawType(implementingClass.loadClass(), resolveTypeArguments(type, implementingClass));
}

private static boolean isNotParametrized(ClassInfo classInfo) {
return classInfo.getTypeSignature() == null || classInfo.getTypeSignature().getTypeParameters().isEmpty();
}
Expand Down
33 changes: 7 additions & 26 deletions src/main/java/com/github/nylle/javafixture/SpecimenFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.github.nylle.javafixture.specimen.ArraySpecimen;
import com.github.nylle.javafixture.specimen.CollectionSpecimen;
import com.github.nylle.javafixture.specimen.EnumSpecimen;
import com.github.nylle.javafixture.specimen.ExperimentalAbstractSpecimen;
import com.github.nylle.javafixture.specimen.GenericSpecimen;
import com.github.nylle.javafixture.specimen.InterfaceSpecimen;
import com.github.nylle.javafixture.specimen.MapSpecimen;
Expand Down Expand Up @@ -47,17 +48,9 @@ public <T> ISpecimen<T> build(final SpecimenType<T> type) {
return new GenericSpecimen<>(type, context, this);
}

if (type.isParameterized() && type.isInterface()) {
if (type.isParameterized() && (type.isInterface() || type.isAbstract())) {
if (context.getConfiguration().experimentalInterfaces()) {
return implementationOrProxy(type);
}

return new GenericSpecimen<>(type, context, this);
}

if (type.isParameterized() && type.isAbstract()) {
if (context.getConfiguration().experimentalInterfaces() && type.isAbstract()) {
return subClassOrProxy(type);
return experimentalAbstract(type);
}

return new GenericSpecimen<>(type, context, this);
Expand All @@ -73,15 +66,15 @@ public <T> ISpecimen<T> build(final SpecimenType<T> type) {

if (type.isInterface()) {
if (context.getConfiguration().experimentalInterfaces()) {
return implementationOrProxy(type);
return experimentalAbstract(type);
}

return new InterfaceSpecimen<>(type, context, this);
}

if (type.isAbstract()) {
if (context.getConfiguration().experimentalInterfaces()) {
return subClassOrProxy(type);
return experimentalAbstract(type);
}
return new AbstractSpecimen<>(type, context, this);
}
Expand All @@ -93,20 +86,8 @@ public <T> ISpecimen<T> build(final SpecimenType<T> type) {
return new ObjectSpecimen<>(type, context, this);
}

private <T> ISpecimen<T> implementationOrProxy(final SpecimenType<T> interfaceType) {
return new ClassPathScanner().findRandomClassFor(interfaceType)
.map(x -> x.isParameterized()
? new GenericSpecimen<>(x, context, this)
: new ObjectSpecimen<>(x, context, this))
.orElseGet(() -> new InterfaceSpecimen<>(interfaceType, context, this));
}

private <T> ISpecimen<T> subClassOrProxy(final SpecimenType<T> abstractType) {
return new ClassPathScanner().findRandomClassFor(abstractType)
.map(x -> x.isParameterized()
? new GenericSpecimen<>(x, context, this)
: new ObjectSpecimen<>(x, context, this))
.orElseGet(() -> new AbstractSpecimen<>(abstractType, context, this));
private <T> ISpecimen<T> experimentalAbstract(SpecimenType<T> interfaceType) {
return new ExperimentalAbstractSpecimen<>(interfaceType, new ClassPathScanner().findAllClassesFor(interfaceType), context, this);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package com.github.nylle.javafixture.specimen;

import com.github.nylle.javafixture.Context;
import com.github.nylle.javafixture.CustomizationContext;
import com.github.nylle.javafixture.ISpecimen;
import com.github.nylle.javafixture.InstanceFactory;
import com.github.nylle.javafixture.SpecimenException;
import com.github.nylle.javafixture.SpecimenFactory;
import com.github.nylle.javafixture.SpecimenType;

import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;

public class ExperimentalAbstractSpecimen<T> implements ISpecimen<T> {

private final SpecimenType<T> type;
private final Context context;
private final SpecimenFactory specimenFactory;
private final InstanceFactory instanceFactory;
private final List<SpecimenType<? extends T>> derivedTypes;

public ExperimentalAbstractSpecimen(SpecimenType<T> type, List<SpecimenType<? extends T>> derivedTypes, Context context, SpecimenFactory specimenFactory) {

if (type == null) {
throw new IllegalArgumentException("type: null");
}

if (derivedTypes == null) {
throw new IllegalArgumentException("derivedTypes: null");
}

if (context == null) {
throw new IllegalArgumentException("context: null");
}

if (specimenFactory == null) {
throw new IllegalArgumentException("specimenFactory: null");
}

if (isNotAbstract(type) || isCollection(type)) {
throw new IllegalArgumentException("type: " + type.getName());
}

this.type = type;
this.context = context;
this.specimenFactory = specimenFactory;
this.instanceFactory = new InstanceFactory(specimenFactory);
this.derivedTypes = derivedTypes;
}

@Override
public T create(CustomizationContext customizationContext, Annotation[] annotations) {
if (context.isCached(type)) {
return context.cached(type);
}

return context.cached(type, shuffledStream(derivedTypes)
.map(derivedType -> specimenFactory.build(derivedType))
.flatMap(derivedSpecimen -> tryCreate(derivedSpecimen, customizationContext, annotations).stream())
.findFirst()
.orElseGet(() -> proxy(customizationContext)));
}

private <R extends T> R proxy(CustomizationContext customizationContext) {
try {
return (R) instanceFactory.proxy(type);
} catch(SpecimenException ex) {
if(type.isAbstract()) {
return (R) instanceFactory.manufacture(type, customizationContext);
}
throw ex;
}
}

private static <T> Optional<T> tryCreate(ISpecimen<T> specimen, CustomizationContext customizationContext, Annotation[] annotations) {
try {
return Optional.of(specimen.create(customizationContext, annotations));
} catch(Exception ex) {
return Optional.empty();
}
}

private static <T> boolean isNotAbstract(SpecimenType<T> type) {
return !(type.isAbstract() || type.isInterface());
}

private static <T> boolean isCollection(SpecimenType<T> type) {
return type.isMap() || type.isCollection();
}

private static <T> Stream<SpecimenType<? extends T>> shuffledStream(List<SpecimenType<? extends T>> list) {
var copy = new ArrayList<>(list);
Collections.shuffle(copy);
return copy.stream();
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,7 @@ public T create(CustomizationContext customizationContext, Annotation[] annotati

private T populate(CustomizationContext customizationContext) {
var result = context.cached(type, instanceFactory.instantiate(type));
var reflector = new Reflector<>(result)
.validateCustomization(customizationContext, type);
var reflector = new Reflector<>(result).validateCustomization(customizationContext, type);
try {
reflector.getDeclaredFields()
.filter(field -> !customizationContext.getIgnoredFields().contains(field.getName()))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,104 +23,104 @@ class ClassPathScannerTest {
private ClassPathScanner sut = new ClassPathScanner();

@Nested
@DisplayName("trying to resolve a random implementation of an interface")
class FindRandomClassForInterface {
@DisplayName("trying to resolve all implementations of an interface class")
class FindAllClassesForInterface {

@Test
@DisplayName("returns an empty Optional if none was found")
void returnsAnEmptyOptionalIfNoneWasFound() {
var actual = sut.findRandomClassFor(SpecimenType.fromClass(InterfaceWithoutImplementation.class));
@DisplayName("returns an empty list if none was found")
void returnsAnEmptyListIfNoneWasFound() {
var actual = sut.findAllClassesFor(SpecimenType.fromClass(InterfaceWithoutImplementation.class));

assertThat(actual).isEmpty();
}

@Test
@DisplayName("returns an empty Optional if not an interface nor abstract")
void returnsAnEmptyOptionalIfNotAnInterfaceNorAbstract() {
var actual = sut.findRandomClassFor(SpecimenType.fromClass(String.class));
@DisplayName("returns an empty list if not an interface nor abstract")
void returnsAnEmptyListIfNotAnInterfaceNorAbstract() {
var actual = sut.findAllClassesFor(SpecimenType.fromClass(String.class));

assertThat(actual).isEmpty();
}

@Test
@DisplayName("returns an empty Optional if exception was thrown")
void returnsAnEmptyOptionalIfExceptionWasThrown() {
@DisplayName("returns an empty list if exception was thrown")
void returnsAnEmptyListIfExceptionWasThrown() {
var throwingType = Mockito.mock(SpecimenType.class);
doThrow(new IllegalArgumentException("expected for test")).when(throwingType).isInterface();

var actual = sut.findRandomClassFor(throwingType);
var actual = sut.findAllClassesFor(throwingType);

assertThat(actual).isEmpty();
}

@Test
@DisplayName("returns a SpecimenType representing an implementing class")
void returnsASpecimenTypeRepresentingAnImplementingClass() {
var actual = sut.findRandomClassFor(SpecimenType.fromClass(InterfaceWithImplementation.class));
@DisplayName("returns a list of SpecimenType all representing an implementing class")
void returnsSpecimenTypesRepresentingImplementingClasses() {
var actual = sut.findAllClassesFor(SpecimenType.fromClass(InterfaceWithImplementation.class));

assertThat(actual).isNotEmpty();
assertThat(actual.get().asClass()).isEqualTo(InterfaceWithImplementationImpl.class);
assertThat(actual).hasSize(1);
assertThat(actual.get(0).asClass()).isEqualTo(InterfaceWithImplementationImpl.class);
}

@Test
@DisplayName("returns a SpecimenType representing an implementing generic class")
void returnsASpecimenTypeRepresentingAnImplementingGenericClass() {
var actual = sut.findRandomClassFor(new SpecimenType<GenericInterfaceTUWithGenericImplementationU<String, Integer>>() {});
@DisplayName("returns a list of SpecimenType all representing an implementing generic class")
void returnsSpecimenTypesRepresentingGenericImplementations() {
var actual = sut.findAllClassesFor(new SpecimenType<GenericInterfaceTUWithGenericImplementationU<String, Integer>>() {});

assertThat(actual).isNotEmpty();
assertThat(actual.get().asClass()).isEqualTo(GenericInterfaceTUWithGenericImplementationUImpl.class);
assertThat(actual.get().getGenericTypeArgument(0).asClass()).isEqualTo(Integer.class);
assertThat(actual).hasSize(1);
assertThat(actual.get(0).asClass()).isEqualTo(GenericInterfaceTUWithGenericImplementationUImpl.class);
assertThat(actual.get(0).getGenericTypeArgument(0).asClass()).isEqualTo(Integer.class);
}
}

@Nested
@DisplayName("trying to resolve a random implementation of an abstract class")
class FindRandomClassForAbstractClass {
@DisplayName("trying to resolve all subclasses of an abstract class")
class FindAllClassesForAbstractClass {

@Test
@DisplayName("returns an empty Optional if none was found")
void returnsAnEmptyOptionalIfNoneWasFound() {
var actual = sut.findRandomClassFor(SpecimenType.fromClass(AbstractClassWithoutImplementation.class));
@DisplayName("returns an empty list if none was found")
void returnsAnEmptyListIfNoneWasFound() {
var actual = sut.findAllClassesFor(SpecimenType.fromClass(AbstractClassWithoutImplementation.class));

assertThat(actual).isEmpty();
}

@Test
@DisplayName("returns an empty Optional if not an interface nor abstract")
void returnsAnEmptyOptionalIfNotAnInterfaceNorAbstract() {
var actual = sut.findRandomClassFor(SpecimenType.fromClass(String.class));
@DisplayName("returns an empty list if not an interface nor abstract")
void returnsAnEmptyListIfNotAnInterfaceNorAbstract() {
var actual = sut.findAllClassesFor(SpecimenType.fromClass(String.class));

assertThat(actual).isEmpty();
}

@Test
@DisplayName("returns an empty Optional if exception was thrown")
void returnsAnEmptyOptionalIfExceptionWasThrown() {
@DisplayName("returns an empty list if exception was thrown")
void returnsAnEmptyListIfExceptionWasThrown() {
var throwingType = Mockito.mock(SpecimenType.class);
doThrow(new IllegalArgumentException("expected for test")).when(throwingType).isInterface();

var actual = sut.findRandomClassFor(throwingType);
var actual = sut.findAllClassesFor(throwingType);

assertThat(actual).isEmpty();
}

@Test
@DisplayName("returns a SpecimenType representing an extending class")
void returnsASpecimenTypeRepresentingAnImplementingClass() {
var actual = sut.findRandomClassFor(SpecimenType.fromClass(AbstractClassWithImplementation.class));
@DisplayName("returns SpecimenTypes all representing a subclass")
void returnsSpecimenTypesRepresentingSubclasses() {
var actual = sut.findAllClassesFor(SpecimenType.fromClass(AbstractClassWithImplementation.class));

assertThat(actual).isNotEmpty();
assertThat(actual.get().asClass()).isEqualTo(AbstractClassWithImplementationImpl.class);
assertThat(actual).hasSize(1);
assertThat(actual.get(0).asClass()).isEqualTo(AbstractClassWithImplementationImpl.class);
}

@Test
@DisplayName("returns a SpecimenType representing an implementing generic class")
void returnsASpecimenTypeRepresentingAnImplementingGenericClass() {
var actual = sut.findRandomClassFor(new SpecimenType<GenericAbstractClassTUWithGenericImplementationU<String, Integer>>() {});
@DisplayName("returns SpecimenTypes all representing an implementing generic class")
void returnsSpecimenTypesRepresentingGenericSubclasses() {
var actual = sut.findAllClassesFor(new SpecimenType<GenericAbstractClassTUWithGenericImplementationU<String, Integer>>() {});

assertThat(actual).isNotEmpty();
assertThat(actual.get().asClass()).isEqualTo(GenericAbstractClassTUWithGenericImplementationUImpl.class);
assertThat(actual.get().getGenericTypeArgument(0).asClass()).isEqualTo(Integer.class);
assertThat(actual).hasSize(1);
assertThat(actual.get(0).asClass()).isEqualTo(GenericAbstractClassTUWithGenericImplementationUImpl.class);
assertThat(actual.get(0).getGenericTypeArgument(0).asClass()).isEqualTo(Integer.class);
}
}
}
Loading

0 comments on commit 1309586

Please sign in to comment.