jqwik icon indicating copy to clipboard operation
jqwik copied to clipboard

Allow Sharing of Generated Values Across Arbitraries

Open jlink opened this issue 4 years ago • 22 comments

Testing Problem

Sometimes a value generated by an arbitrary should be the same one for all usages of this arbitrary in the same try. Currently, this can only be accomplished by flat mapping over this value and handing the value down to all other arbitraries.

It would simplify some properties and arbitrary definitions if there was a simpler way to accomplish the same effect

Suggested Solution

There are at least two situations in which you might want to use this feature:

  1. In Arbitrary Creation API. E.g.:

    Arbitrary.share(String key)
    

    This could create an arbitrary that will only generate a value the first time it is used with the same key! Look at a similar Hypothesis feature

  2. When using as property parameters, e.g.:

    @Property
    void myProperty(@ForAll("fullNames") String aString, @ForAll(sharedBy="firstName") String firstName) { }
    
    @Provide
    Arbitrary<String> fullNames() {
        Arbitrary<String> firstName = Arbitraries.strings().share("firstName");
        Arbitrary<String> lastName = Arbitraries.strings().share("lastName");
        return Combinators.combine(firstName, lastName).as( (f, l) -> f + " " + l);
    }
    

Discussion

Implementation has a few complications:

  • One has to make sure that the same sharing Id is only used once per try
  • Shrinking may be a problem unless there will be flat mapping under the hood. This has not been fully researched yet, though.

Sketched some half-working code: https://github.com/jlink/jqwik/blob/main/engine/src/test/java/experiments/SharedArbitraryExperiments.java

jlink avatar Jan 10 '22 08:01 jlink

Frankly speaking, I would love to hide combine and flatMap into jqwik implementation. Of course, there will always be cases when flatMap would be useful, however, I would like to write code like

@Property
void myProperty(@ForAll("fullNames") String aString, @ForAll("firstNames") String firstName) { }

// It might come from extension or something like that
@Provide
Arbitrary<String> firstNames() {...}
@Provide
Arbitrary<String> lastNames() {...}

@Provide
Arbitrary<String> fullNames(@ForAll("firstNames") String firstName, @ForAll("lastNames") lastName) {
    return firstName + lastName;
}

vlsi avatar Jan 10 '22 16:01 vlsi

I would like to write code like

Have you tried it? Should already work.

jlink avatar Jan 10 '22 16:01 jlink

Would that feature be useful if shared values couldn't be shrunk?

Without shrinking this is rather easy to implement...

jlink avatar Jan 11 '22 14:01 jlink

What is the "easy" implementation you have in mind? I thought the implementation would be to arrange the needed combine behind the scenes, then it should support shrinking, shouldn't it?

vlsi avatar Jan 11 '22 15:01 vlsi

The easy implementation is to just store the generated unshrinkable value on first access.

Going for a full flatmap and flat combine behind the scenes would IMO require a data flow analysis across all generated values. I don’t even know if that’s possible. There may be a more hacky solution making use of stores and try lifecycle, but I could go with a first unshrinkable solution, if it provides value.

jlink avatar Jan 11 '22 16:01 jlink

I see.

@Provide
Arbitrary<String> fullNames() {
    Arbitrary<String> firstName = Arbitraries.strings().share("firstName");
    Arbitrary<String> lastName = Arbitraries.strings().share("lastName");
    return Combinators.combine(firstName, lastName).as( (f, l) -> f + " " + l);
}

it does indeed need dataflow analysis.

However, is dataflow needed if all the "sharing" is done via method parameters and return values?

vlsi avatar Jan 11 '22 17:01 vlsi

In other words, the definition like

// combine, works
@Provide
Arbitrary<String> fullNames(@ForAll("firstNames") String firstName, @ForAll("lastNames") lastName) {
    return just(firstName + lastName);
}


// combine, works
@Provide
Arbitrary<String> fullNames(@ForAll("firstNames") String firstName, @ForAll("lastNames") lastName) {
   ints(..)
...

vs

@Provide
Arbitrary<String> fullNames(@ForAll("firstNames") String firstName, @ForAll("lastNames") lastName, @ForAll int ints) {

means fullNames is a combination of firstNames and lastNames. There's no need to analyze the code, and there's no need to execute the code.

vlsi avatar Jan 11 '22 18:01 vlsi

@Provide
Arbitrary<String> fullNames(@ForAll("firstNames") String firstName, @ForAll("lastNames") lastName) {
    return firstName + lastName;
}

That code doesn’t compile though. Jqwik needs a clear criterion to decide if it has to flatMap/combine over parameters or if plain map/combine suffices. A return type String would also work but then you could never map/combine to an Arbitrary object, since this would signal a flatMap/combine.

A new annotation would do the trick, but there are already too many for my taste.

jlink avatar Jan 11 '22 19:01 jlink

I suggest it just uses combine

vlsi avatar Jan 11 '22 19:01 vlsi

By the way, it might make sense to organize a call to explore options. Wdyt?

vlsi avatar Jan 11 '22 19:01 vlsi

Sure, when would it suit you?

jlink avatar Jan 12 '22 09:01 jlink

E.g. https://doodle.com/mm/vladimirsitnikov/30min-meeting

vlsi avatar Jan 12 '22 10:01 vlsi

Did some experimentation in https://github.com/jlink/jqwik/blob/main/engine/src/test/java/experiments/SharedArbitraryExperiments.java

jlink avatar Jan 12 '22 10:01 jlink

One more idea. What if @ForAll could also resolve Arbitrary types. Like

@Provide
Arbitrary<String> aString(@ForAll Arbitrary<Integer> ints) {
	return ints.map(i -> i.toString());
}

This would not hide map/flatMap/combine but it could be used directly in provider methods.

jlink avatar Jan 12 '22 10:01 jlink

What if @ForAll could also resolve Arbitrary types. Like

That would be useful as well.

vlsi avatar Jan 12 '22 11:01 vlsi

That was so easy to implement that I just did it. Now this works:

@Property
void test(@ForAll("chessSquares") String square) {
	System.out.println(square);
}

@Provide
Arbitrary<String> chessSquares(
	@ForAll Arbitrary<@CharRange(from = 'a', to = 'h') Character> rows,
	@ForAll Arbitrary<@IntRange(min = 1, max = 8) Integer> columns
) {
	return Combinators.combine(rows, columns).as((r, c) -> r.toString() + c);
}

Released and available in "1.6.3-SNAPSHOT"

jlink avatar Jan 12 '22 11:01 jlink

@vlsi Would @InjectArbitrary be more understandable than @ForAll in this case?

jlink avatar Jan 13 '22 14:01 jlink

@InjectArbitrary Arbitrary<Integer> ints sounds like a duplication though.

vlsi avatar Jan 13 '22 14:01 vlsi

@Inject seemed too generic, but I'm unsure.

jlink avatar Jan 13 '22 14:01 jlink

@Inject might make sense if there is more than one type of injectable item (e.g. Arbitrary, some sort of jqwik-related service, something related to test feedback, etc). I don't know if that is the case.

vlsi avatar Jan 13 '22 15:01 vlsi

Currently, I don't see any other types.

jlink avatar Jan 13 '22 15:01 jlink

What about @ForArbitrary?

jlink avatar Jan 18 '22 07:01 jlink