junit-quickcheck
junit-quickcheck copied to clipboard
Allow deallocation of generated values
Allocating new values by calling Generator<T>.generate() will create new object instances for each trial and shrink iteration. This is working just fine for simple objects, but will not allow you to destroy objects that have been allocating external resources. E.g. a FileGenerator may want to delete any generated files after the generated value is not needed anymore. There should be a way to tear down more complex generated values.
Possible approaches that come to my mind would be:
- Add
destroy(Object)method toGeneratorthat must be called with any object created bygenerate(). Simple to implement, but contract will be hard to enforce. - Allow to configure an optional
GenerationCollectorimplementation that will receive all objects that have been generated duringPropertyStatement.verifyProperty()after the method returned. TheGenerationCollectorimplementation would have to figure out which resources need to be deallocated and how, e.g.obj instanceof File && obj.delete(). The hard part would be how to track which objects have been generated. - Do not offer any contract for this at all, but allow generators to be notified whenever a
PropertyStatement.verifyProperty()iteration has finished. The generator could temporary store references to any resources that need to be deallocated and free them after each iteration.
@spodkowinski Thanks for this -- sorry I'm so late in responding.
I am leaning toward option 1 above. Will experiment with it a bit and see how it works.
@spodkowinski The tricky part, I think, will be how to handle generated objects that never make it to a PropertyVerifier. For example:
- In the presence of a
@When(satisfies = condition)annotation, values may get generated for a parameter but not be accepted. - Values produced by a generator outfitted with
Gen.filter()-- same issue.
Maybe not so bad -- let me bang on it some more.
@spodkowinski Should a "disposal" swallow any exceptions that it may raise? Log them?
@spodkowinski I'm beginning to wonder whether or not leaving this capability out hinders the functionality of junit-quickcheck enough to warrant adding the additional complexity.
What workarounds are there? Property methods that receive generated objects that claim external resources could dispose of them, at the cost of some extra code in the test class.
Perhaps this and gh-113 are two sides of the same coin. Allowing for an @Before and @After type construct that accepts parameters might solve the problem. It might make sense to have the @After be given the same values as the @Before, rather than generating new ones.
@spodkowinski @m0smith One option might be to create a Rule that can hold values to be deallocated:
public static class FileDisposer extends ExternalResource {
private final List<File> candidates = new ArrayList<>();
public void collect(File first, File... rest) {
candidates.add(first);
Collections.addAll(candidates, rest);
}
@Override protected void after() {
candidates.forEach(File::delete);
}
}
@RunWith(JUnitQuickcheck.class)
public static class FileProperties {
@Rule public final FileDisposer d = new FileDisposer();
@Property public void holds(File f) {
d.collect(f);
// ...
}
}
The cost to you, the programmer, is the creation of the Rule and the collecting of the values you want disposed in the property method itself. Cost to junit-quickcheck: no additional code needed, since junit-quickcheck honors Rules and other such JUnit machinery.
Let me know what you think.
Would the rule be called on each trial or just after the property method evaluation finished? What about the mentioned values that won't make it into the test method as there are filtered out?
@pholser I like the idea of leveraging Rule annotated instances to do this as it is part of junit already. An example in the documentation would be great as I wasn't even aware of the Rule annotation until you pointed it out.
I also think that having a Rule with Prime methods (see comments in gh-113) would be a good addition sometime in the future.
@spodkowinski The rule would wrap each trial, including shrinks. Thus, its after method would follow every trial.
The rule won't catch any values filtered by the methods mentioned above.