Skip to content

Commit 1309586

Browse files
committed
replace random by strategic specimen-selection
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.
1 parent 3f612ea commit 1309586

12 files changed

+491
-287
lines changed

src/main/java/com/github/nylle/javafixture/ClassPathScanner.java

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,31 +6,15 @@
66

77
import java.lang.reflect.Type;
88
import java.util.List;
9-
import java.util.Optional;
10-
import java.util.Random;
119
import java.util.stream.Collectors;
1210

1311
public class ClassPathScanner {
1412

15-
public <T> Optional<SpecimenType<T>> findRandomClassFor(SpecimenType<T> type) {
13+
public <T> List<SpecimenType<? extends T>> findAllClassesFor(SpecimenType<T> type) {
1614
try (ScanResult scanResult = new ClassGraph().enableAllInfo().scan()) {
17-
18-
var result = filter(scanResult, type);
19-
20-
if (result.isEmpty()) {
21-
return Optional.empty();
22-
}
23-
24-
var implementingClass = result.get(new Random().nextInt(result.size()));
25-
26-
if (isNotParametrized(implementingClass)) {
27-
return Optional.of(SpecimenType.fromClass(implementingClass.loadClass()));
28-
}
29-
30-
return Optional.of(SpecimenType.fromRawType(implementingClass.loadClass(), resolveTypeArguments(type, implementingClass)));
31-
15+
return filter(scanResult, type).stream().map(x -> specimenTypeOf(x, type)).collect(Collectors.toList());
3216
} catch (Exception ex) {
33-
return Optional.empty();
17+
return List.of();
3418
}
3519
}
3620

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

40+
private static <T, R extends T> SpecimenType<R> specimenTypeOf(ClassInfo implementingClass, SpecimenType<T> type) {
41+
if (isNotParametrized(implementingClass)) {
42+
return SpecimenType.fromClass(implementingClass.loadClass());
43+
}
44+
45+
return SpecimenType.fromRawType(implementingClass.loadClass(), resolveTypeArguments(type, implementingClass));
46+
}
47+
5648
private static boolean isNotParametrized(ClassInfo classInfo) {
5749
return classInfo.getTypeSignature() == null || classInfo.getTypeSignature().getTypeParameters().isEmpty();
5850
}

src/main/java/com/github/nylle/javafixture/SpecimenFactory.java

Lines changed: 7 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import com.github.nylle.javafixture.specimen.ArraySpecimen;
55
import com.github.nylle.javafixture.specimen.CollectionSpecimen;
66
import com.github.nylle.javafixture.specimen.EnumSpecimen;
7+
import com.github.nylle.javafixture.specimen.ExperimentalAbstractSpecimen;
78
import com.github.nylle.javafixture.specimen.GenericSpecimen;
89
import com.github.nylle.javafixture.specimen.InterfaceSpecimen;
910
import com.github.nylle.javafixture.specimen.MapSpecimen;
@@ -47,17 +48,9 @@ public <T> ISpecimen<T> build(final SpecimenType<T> type) {
4748
return new GenericSpecimen<>(type, context, this);
4849
}
4950

50-
if (type.isParameterized() && type.isInterface()) {
51+
if (type.isParameterized() && (type.isInterface() || type.isAbstract())) {
5152
if (context.getConfiguration().experimentalInterfaces()) {
52-
return implementationOrProxy(type);
53-
}
54-
55-
return new GenericSpecimen<>(type, context, this);
56-
}
57-
58-
if (type.isParameterized() && type.isAbstract()) {
59-
if (context.getConfiguration().experimentalInterfaces() && type.isAbstract()) {
60-
return subClassOrProxy(type);
53+
return experimentalAbstract(type);
6154
}
6255

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

7467
if (type.isInterface()) {
7568
if (context.getConfiguration().experimentalInterfaces()) {
76-
return implementationOrProxy(type);
69+
return experimentalAbstract(type);
7770
}
7871

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

8275
if (type.isAbstract()) {
8376
if (context.getConfiguration().experimentalInterfaces()) {
84-
return subClassOrProxy(type);
77+
return experimentalAbstract(type);
8578
}
8679
return new AbstractSpecimen<>(type, context, this);
8780
}
@@ -93,20 +86,8 @@ public <T> ISpecimen<T> build(final SpecimenType<T> type) {
9386
return new ObjectSpecimen<>(type, context, this);
9487
}
9588

96-
private <T> ISpecimen<T> implementationOrProxy(final SpecimenType<T> interfaceType) {
97-
return new ClassPathScanner().findRandomClassFor(interfaceType)
98-
.map(x -> x.isParameterized()
99-
? new GenericSpecimen<>(x, context, this)
100-
: new ObjectSpecimen<>(x, context, this))
101-
.orElseGet(() -> new InterfaceSpecimen<>(interfaceType, context, this));
102-
}
103-
104-
private <T> ISpecimen<T> subClassOrProxy(final SpecimenType<T> abstractType) {
105-
return new ClassPathScanner().findRandomClassFor(abstractType)
106-
.map(x -> x.isParameterized()
107-
? new GenericSpecimen<>(x, context, this)
108-
: new ObjectSpecimen<>(x, context, this))
109-
.orElseGet(() -> new AbstractSpecimen<>(abstractType, context, this));
89+
private <T> ISpecimen<T> experimentalAbstract(SpecimenType<T> interfaceType) {
90+
return new ExperimentalAbstractSpecimen<>(interfaceType, new ClassPathScanner().findAllClassesFor(interfaceType), context, this);
11091
}
11192
}
11293

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package com.github.nylle.javafixture.specimen;
2+
3+
import com.github.nylle.javafixture.Context;
4+
import com.github.nylle.javafixture.CustomizationContext;
5+
import com.github.nylle.javafixture.ISpecimen;
6+
import com.github.nylle.javafixture.InstanceFactory;
7+
import com.github.nylle.javafixture.SpecimenException;
8+
import com.github.nylle.javafixture.SpecimenFactory;
9+
import com.github.nylle.javafixture.SpecimenType;
10+
11+
import java.lang.annotation.Annotation;
12+
import java.util.ArrayList;
13+
import java.util.Collections;
14+
import java.util.List;
15+
import java.util.Optional;
16+
import java.util.stream.Stream;
17+
18+
public class ExperimentalAbstractSpecimen<T> implements ISpecimen<T> {
19+
20+
private final SpecimenType<T> type;
21+
private final Context context;
22+
private final SpecimenFactory specimenFactory;
23+
private final InstanceFactory instanceFactory;
24+
private final List<SpecimenType<? extends T>> derivedTypes;
25+
26+
public ExperimentalAbstractSpecimen(SpecimenType<T> type, List<SpecimenType<? extends T>> derivedTypes, Context context, SpecimenFactory specimenFactory) {
27+
28+
if (type == null) {
29+
throw new IllegalArgumentException("type: null");
30+
}
31+
32+
if (derivedTypes == null) {
33+
throw new IllegalArgumentException("derivedTypes: null");
34+
}
35+
36+
if (context == null) {
37+
throw new IllegalArgumentException("context: null");
38+
}
39+
40+
if (specimenFactory == null) {
41+
throw new IllegalArgumentException("specimenFactory: null");
42+
}
43+
44+
if (isNotAbstract(type) || isCollection(type)) {
45+
throw new IllegalArgumentException("type: " + type.getName());
46+
}
47+
48+
this.type = type;
49+
this.context = context;
50+
this.specimenFactory = specimenFactory;
51+
this.instanceFactory = new InstanceFactory(specimenFactory);
52+
this.derivedTypes = derivedTypes;
53+
}
54+
55+
@Override
56+
public T create(CustomizationContext customizationContext, Annotation[] annotations) {
57+
if (context.isCached(type)) {
58+
return context.cached(type);
59+
}
60+
61+
return context.cached(type, shuffledStream(derivedTypes)
62+
.map(derivedType -> specimenFactory.build(derivedType))
63+
.flatMap(derivedSpecimen -> tryCreate(derivedSpecimen, customizationContext, annotations).stream())
64+
.findFirst()
65+
.orElseGet(() -> proxy(customizationContext)));
66+
}
67+
68+
private <R extends T> R proxy(CustomizationContext customizationContext) {
69+
try {
70+
return (R) instanceFactory.proxy(type);
71+
} catch(SpecimenException ex) {
72+
if(type.isAbstract()) {
73+
return (R) instanceFactory.manufacture(type, customizationContext);
74+
}
75+
throw ex;
76+
}
77+
}
78+
79+
private static <T> Optional<T> tryCreate(ISpecimen<T> specimen, CustomizationContext customizationContext, Annotation[] annotations) {
80+
try {
81+
return Optional.of(specimen.create(customizationContext, annotations));
82+
} catch(Exception ex) {
83+
return Optional.empty();
84+
}
85+
}
86+
87+
private static <T> boolean isNotAbstract(SpecimenType<T> type) {
88+
return !(type.isAbstract() || type.isInterface());
89+
}
90+
91+
private static <T> boolean isCollection(SpecimenType<T> type) {
92+
return type.isMap() || type.isCollection();
93+
}
94+
95+
private static <T> Stream<SpecimenType<? extends T>> shuffledStream(List<SpecimenType<? extends T>> list) {
96+
var copy = new ArrayList<>(list);
97+
Collections.shuffle(copy);
98+
return copy.stream();
99+
}
100+
}
101+

src/main/java/com/github/nylle/javafixture/specimen/ObjectSpecimen.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,7 @@ public T create(CustomizationContext customizationContext, Annotation[] annotati
5959

6060
private T populate(CustomizationContext customizationContext) {
6161
var result = context.cached(type, instanceFactory.instantiate(type));
62-
var reflector = new Reflector<>(result)
63-
.validateCustomization(customizationContext, type);
62+
var reflector = new Reflector<>(result).validateCustomization(customizationContext, type);
6463
try {
6564
reflector.getDeclaredFields()
6665
.filter(field -> !customizationContext.getIgnoredFields().contains(field.getName()))

src/test/java/com/github/nylle/javafixture/ClassPathScannerTest.java

Lines changed: 44 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -23,104 +23,104 @@ class ClassPathScannerTest {
2323
private ClassPathScanner sut = new ClassPathScanner();
2424

2525
@Nested
26-
@DisplayName("trying to resolve a random implementation of an interface")
27-
class FindRandomClassForInterface {
26+
@DisplayName("trying to resolve all implementations of an interface class")
27+
class FindAllClassesForInterface {
2828

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

3434
assertThat(actual).isEmpty();
3535
}
3636

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

4242
assertThat(actual).isEmpty();
4343
}
4444

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

51-
var actual = sut.findRandomClassFor(throwingType);
51+
var actual = sut.findAllClassesFor(throwingType);
5252

5353
assertThat(actual).isEmpty();
5454
}
5555

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

61-
assertThat(actual).isNotEmpty();
62-
assertThat(actual.get().asClass()).isEqualTo(InterfaceWithImplementationImpl.class);
61+
assertThat(actual).hasSize(1);
62+
assertThat(actual.get(0).asClass()).isEqualTo(InterfaceWithImplementationImpl.class);
6363
}
6464

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

70-
assertThat(actual).isNotEmpty();
71-
assertThat(actual.get().asClass()).isEqualTo(GenericInterfaceTUWithGenericImplementationUImpl.class);
72-
assertThat(actual.get().getGenericTypeArgument(0).asClass()).isEqualTo(Integer.class);
70+
assertThat(actual).hasSize(1);
71+
assertThat(actual.get(0).asClass()).isEqualTo(GenericInterfaceTUWithGenericImplementationUImpl.class);
72+
assertThat(actual.get(0).getGenericTypeArgument(0).asClass()).isEqualTo(Integer.class);
7373
}
7474
}
7575

7676
@Nested
77-
@DisplayName("trying to resolve a random implementation of an abstract class")
78-
class FindRandomClassForAbstractClass {
77+
@DisplayName("trying to resolve all subclasses of an abstract class")
78+
class FindAllClassesForAbstractClass {
7979

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

8585
assertThat(actual).isEmpty();
8686
}
8787

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

9393
assertThat(actual).isEmpty();
9494
}
9595

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

102-
var actual = sut.findRandomClassFor(throwingType);
102+
var actual = sut.findAllClassesFor(throwingType);
103103

104104
assertThat(actual).isEmpty();
105105
}
106106

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

112-
assertThat(actual).isNotEmpty();
113-
assertThat(actual.get().asClass()).isEqualTo(AbstractClassWithImplementationImpl.class);
112+
assertThat(actual).hasSize(1);
113+
assertThat(actual.get(0).asClass()).isEqualTo(AbstractClassWithImplementationImpl.class);
114114
}
115115

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

121-
assertThat(actual).isNotEmpty();
122-
assertThat(actual.get().asClass()).isEqualTo(GenericAbstractClassTUWithGenericImplementationUImpl.class);
123-
assertThat(actual.get().getGenericTypeArgument(0).asClass()).isEqualTo(Integer.class);
121+
assertThat(actual).hasSize(1);
122+
assertThat(actual.get(0).asClass()).isEqualTo(GenericAbstractClassTUWithGenericImplementationUImpl.class);
123+
assertThat(actual.get(0).getGenericTypeArgument(0).asClass()).isEqualTo(Integer.class);
124124
}
125125
}
126126
}

0 commit comments

Comments
 (0)