Open SimY4 opened 1 year ago
I’m not perfectly sure what you mean by „preconfigured“ but I assume you mean a type enriched by one or more annotation? If so, the answer is no. Without thinking deeply about it I’d say the use case is quite narrow, since there’s no way in Java I know of (without byte code manipulation anyway) to instantiate an annotation with some properties set. What remains is the case of a type with just plain annotations.
@jlink by pre-configured I mean it went through all applicable ArbitraryConfigurator instances available.
My usecase is essentially creating an arbitrary that can pass down the configuration info from itself. Imagine a product type.
A { b: B, c: C }
if B and C can be configured with annotation D I'd like an A to also be configured with D and essentially pass down annotation info to B and C.
// in arbitrary provider
@Override
public Set<Arbitrary<?>> provideFor(TypeUsage targetType, SubtypeProvider subtypeProvider) {
var maybeD = targetType.findAnnotation(D.class)
Combinators.combine(
Arbitraries.defaultFor(B.class), // pass D
Arbitraries.defaultFor(C.class). // pass D
).as(A::new)
Configurators are not handed down to an arbitrary‘s children. If they were, one couldn’t be precise with where to apply them since some could be applicable to both parent and children. What if you want it only for the parent?
Maybe you can show concrete use cases and why the current API is a bad fit. There may be a new feature hiding there.
@jlink somewhat abstracted use-case of mine. Let's assume I have my own real number type:
class Money(integer: long, fraction: long) extends Number
And I have configurator I call small, that comes with annotation @Small that's applicable for longs. Essentially just generates only small long values.
Now, I can say that my Money type can also be small if I can pass down the annotation from Money down to longs.
I don't want to write a configurator for Money. If I can just summon the right instances for longs based on annotations present on the type.
Now, I can say that my Money type can also be small if I can pass down the annotation from Money down to longs.
So the problem I described above holds:
Configurators are not handed down to an arbitrary‘s children. If they were, one couldn’t be precise with where to apply them since some could be applicable to both parent and children. What if you want it only for the parent?
I'm not sure how to mitigate that. One could introduce a meta annotation to specify if an annotation is handed down or not, but that introduces another bag of complications.
Have you tried using domains for your use case? If your domain-specific long generator was defined within the domain then it would be picked up automatically in a defaultFor
call. Domains also give a clear boundary of when you want to apply what defaults.
@jlink
Configurators are not handed down to an arbitrary‘s children.
They are not but they are easily accessible on a parent instance and once instantiated annotation doesn't hold any connections to the parent type so can be used as an annotation on an inner type.
One way to solve my problem is by introducing a wrapper type:
record Small<T extends Number>(value: T) extends Number {}
Then, instead of creating configurations for small longs, i'd create an arbitrary for a Small<Long>
and then arbitrary for a Small<Money>
would summon two instances of Small<Long>
s.
This works. But wrappers are not composing really well. If you imagine a scenario where I have three ways of restricting a type:
Arbtrary<Small<Odd<Positive<Long>>>>
you'd have to either use these restrictions in a specific order or provide arbitraries for exponentially growing combinations of these restrictors. Oppose to
Arbitrary<@Odd @Small @Positive Long>
where you just pipe them through the list of configurations.
Have you tried using domains for your use case?
tbh, I'm not sold on the idea of domains. because the use of domain immediately removes access for globally available arbitrary instances (you have to include them explicitly) it's easy to miss applicable instances and therefore skip valuable tests once you're in domains.
tbh, I'm not sold on the idea of domains. because the use of domain immediately removes access for globally available arbitrary instances (you have to include them explicitly) it's easy to miss applicable instances and therefore skip valuable tests once you're in domains
A domain class can itself have annotation
@Domain(DomainContext.Global.class)
which will automatically import the „normal“ resolvers for users of your domain.
I still think domains are your best choice here, but you could also create an arbitrary provider yourself for types where handing down annotations is warranted:
class MyProviderForA implements ArbitraryProvider {
@Override
public boolean canProvideFor(TypeUsage targetType) {
return targetType.isAssignableFrom(A.class);
}
private TypeUsage addAnnotations(List<Annotation> annotations, TypeUsage type) {
if (annotations.isEmpty()) {
return type;
}
Annotation annotation = annotations.get(0);
return addAnnotations(annotations.subList(1, annotations.size()), type.withAnnotation(annotation));
}
@Override
public Set<Arbitrary<?>> provideFor(TypeUsage targetType, SubtypeProvider subtypeProvider) {
var annotations = targetType.getAnnotations()
Combinators.combine(
addAnnotations(annotations, Arbitraries.defaultFor(B.class)),
addAnnotations(annotations, Arbitraries.defaultFor(C.class))
).as(A::new)
}
}
@jlink this is what I do today. With a minor tweak that my addAnnotations
is essentially just manually calling to configurer, I.e.:
private final SmallConfigurer configurer = new SmallConfigurer()
@Override
public Set<Arbitrary<?>> provideFor(TypeUsage targetType, SubtypeProvider subtypeProvider) {
var small = targetType.getAnnotation(Small.class)
Combinators.combine(
configurer.configureB(Arbitraries.defaultFor(B.class), small),
configurer.configureC(Arbitraries.defaultFor(C.class), small)
).as(A::new)
}
That's why I was wondering if you can just have an api to take this responsibility into jqwik. But that's cool if it's not part of jqwik, I just decided to ask.
I am open to considering an additional API just not to routinely handing down annotations.
Do you have a suggestion how this API could look and fit into the existing ones?
@jlink my original thought was (and TBH I still think it's the best of all options) to consider annotation instances added onto TypeUsage
to look up configurations:
Arbitraries.defaultFor(TypeUsage.of(MyClass.class).withAnnotation(<annotation instance>))
(+) this API exists today and doesn't require new extensions (+) TypeUsage already supports attaching annotations to it (!) It's a behavioural breaking change so maybe better to have an alternative API if these types of changes are considered as breaking.
Some other options (or rather state of the art in other places that I saw) that I personally think worse than this one:
Arbitraries.defaultFor(new TypeToken<@Small Long>() {});
(+) captures annotations on types (-) pollutes userspace with anonymous classes (!) somewhat hard to use
private final @Small long smallLong;
@Override
public Set<Arbitrary<?>> provideFor(TypeUsage targetType, SubtypeProvider subtypeProvider) {
Arbitrary<Long> smallLong = Arbitraries.defaultFor(getClass().getField("smallLong"));
}
(+) captures annotations on types (-) checked reflection exceptions everywhere (!) somewhat hard to use and easy to mess up
Arbitraries.defaultFor(TypeUsage.of(MyClass.class).withAnnotation(<annotation instance>))
This one already works, if you can grab an instance of an annotation from somewhere. It does not propagate it down, and it never will because this would make the application of annotations ambiguous (see further up in the discussion).
I could introduce TypeUsage.withAnnotationClass(Class<? extends Annotation> anClass)
but that would not solve the propagation issue either and only allow for annotations without additional values.
Drawing from your Junit Quickcheck example, what about a new way to create TypeUsage
instances like that:
private final @Small long smallLong;
@Override
public Set<Arbitrary<?>> provideFor(TypeUsage targetType, SubtypeProvider subtypeProvider) {
Arbitrary<Long> smallLong = Arbitraries.defaultFor(
TypeUsage.fromField(getClass().getField("smallLong"))
);
}
The logic for doing that is probably already present somewhere in the engine's code, so it'd be just a matter of cleaning it up and presenting it as a public interface.
@jlink
This one already works, if you can grab an instance of an annotation from somewhere.
hmmm, indeed... Though I think it messes with the cashes again in some nasty way, because:
this works ok:
configurer.configureB(Arbitraries.defaultFor(B.class), small)
this doesn't work:
Arbitraries.defaultFor(TypeUsage.of(B.class).withAnnotation(small))
In my case if fails with the configurator essentially filtering too many values (10000). Somehow the same doesn't occur if I summon first and then configure.
In my case if fails with the configurator essentially filtering too many values (10000). Somehow the same doesn't occur if I summon first and then configure.
Depending on how the small configurator works the order of application could play a role. E.g. first constraining the value range and then filtering might work, the other way around might lead to too many misses.
Not sure if it will fit your use case, but I'll share how I'm doing it. It is however not annotation driven. What I'm doing is creating an interface like FooArbitraries
. There, I define a handful of default
methods annotated with @Provide
and implement them suitable to my needs.
Then it becomes plug and play. For tests, a test class would then implement FooArbitraries
(or however many others). Define a bunch of String
fields in FooArbitraries
interface mirroring names of provider methods so that values can be easily injected in test methods - all you have to do is use @ForAll(<arbitrary name>)
and IntelliJ will even give you autocompletion. For combining arbitraries like you want, you'd have something like NumberBoundariesArbitraries
with defined boundaries that emit number ranges that you want. Your MoneyArbitraries
would then extend NumberBoundariesArbitraries
and Money provider would re-use the Arbitrary
constrained to the range you had defined earlier. Again, expose method names through String
fields in MoneyArbitraries
, make your test implement MoneyArbitraries
and easily inject them in your tests. Somewhat messy and less fancy approach, but gets the job done.
Quick question, maybe I'm missing something in the doc.
I can summon arbitraries in PBTs and @Provide annotated methods with configuration annotations.
Is there a handy way to summon already pre-configured arbitrary instances in an imperative way? I was hoping this will do the trick:
Arbitraries.defaultFor(TypeUsage.of(MyClass.class).withAnnotation(Annotation))
but looks like it doesn't.