Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
RELEASE_TYPE: patch

This patch adds `Generators.optionalInts`, `Generators.optionalLongs` and `Generators.optionalDoubles`,
which generate the primitive optionals `OptionalInt`, `OptionalLong` and `OptionalDouble` the same way
`Generators.optional` generates `Optional<T>` ([#27](https://github.com/hegeldev/hegel-java/issues/27)).
Each one generates either an empty optional or a value from the generator you pass in, and shrinks
toward empty.

```java
Generator<OptionalLong> max = Generators.optionalLongs(Generators.longs().min(0).max(100));
```

`Generators.forType` and `Generators.records` now also derive generators for these types, so a record
like `record Stats(long count, OptionalLong max, OptionalDouble mean)` no longer fails with
`No default generator for java.util.OptionalLong`.
39 changes: 36 additions & 3 deletions shared/src/main/java/dev/hegel/Generators.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.OptionalLong;
import java.util.UUID;
import java.util.function.Function;

Expand Down Expand Up @@ -198,6 +201,36 @@ public static <T> Generator<Optional<T>> optional(Generator<T> generator) {
return oneOf(just(Optional.<T>empty()), generator.map(Optional::of));
}

/**
* Generates an {@link OptionalInt} that is empty or wraps a value from {@code generator}.
*
* @param generator the value generator
* @return an optional-int generator
*/
public static Generator<OptionalInt> optionalInts(Generator<Integer> generator) {
return oneOf(just(OptionalInt.empty()), generator.map(OptionalInt::of));
}

/**
* Generates an {@link OptionalLong} that is empty or wraps a value from {@code generator}.
*
* @param generator the value generator
* @return an optional-long generator
*/
public static Generator<OptionalLong> optionalLongs(Generator<Long> generator) {
return oneOf(just(OptionalLong.empty()), generator.map(OptionalLong::of));
}

/**
* Generates an {@link OptionalDouble} that is empty or wraps a value from {@code generator}.
*
* @param generator the value generator
* @return an optional-double generator
*/
public static Generator<OptionalDouble> optionalDoubles(Generator<Double> generator) {
return oneOf(just(OptionalDouble.empty()), generator.map(OptionalDouble::of));
}

// --- collections ---

/**
Expand Down Expand Up @@ -465,9 +498,9 @@ public static <T> Deferred<T> deferred() {
* Derive a generator for {@code type} by reflection.
*
* <p>Supports scalar types ({@code int}, {@code long}, {@code boolean}, {@code float}, {@code
* double}, {@code String}, {@code byte[]}, {@link UUID}, and their wrappers), enums, records
* (recursively), and {@code List}, {@code Set}, {@code Optional} and {@code Map} of supported
* element types.
* double}, {@code String}, {@code byte[]}, {@link UUID}, and their wrappers), {@link
* OptionalInt}, {@link OptionalLong}, {@link OptionalDouble}, enums, records (recursively), and
* {@code List}, {@code Set}, {@code Optional} and {@code Map} of supported element types.
*
* @param type the type to derive a generator for
* @param <T> the type
Expand Down
16 changes: 13 additions & 3 deletions shared/src/main/java/dev/hegel/generators/Derive.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@

/**
* Reflection-based generator derivation. Maps a Java type to a default generator: scalar wrappers
* to their primitive generators, enums to a choice over their constants, records to a generator
* over their components, and {@code List}/{@code Set}/{@code Optional}/{@code Map} to the matching
* collection generators (using the declared element types).
* to their primitive generators, {@code OptionalInt}/{@code OptionalLong}/{@code OptionalDouble} to
* optionals over them, enums to a choice over their constants, records to a generator over their
* components, and {@code List}/{@code Set}/{@code Optional}/{@code Map} to the matching collection
* generators (using the declared element types).
*/
public final class Derive {
private Derive() {}
Expand Down Expand Up @@ -64,6 +65,15 @@ private static Generator<?> scalar(Class<?> cls) {
if (cls == double.class || cls == Double.class) {
return Generators.doubles();
}
if (cls == java.util.OptionalInt.class) {
return Generators.optionalInts(Generators.integers());
}
if (cls == java.util.OptionalLong.class) {
return Generators.optionalLongs(Generators.longs());
}
if (cls == java.util.OptionalDouble.class) {
return Generators.optionalDoubles(Generators.doubles());
}
if (cls == String.class) {
return Generators.text();
}
Expand Down
42 changes: 42 additions & 0 deletions shared/src/test/java/dev/hegel/ConformanceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
import static dev.hegel.Generators.maps;
import static dev.hegel.Generators.oneOf;
import static dev.hegel.Generators.optional;
import static dev.hegel.Generators.optionalDoubles;
import static dev.hegel.Generators.optionalInts;
import static dev.hegel.Generators.optionalLongs;
import static dev.hegel.Generators.sampledFrom;
import static dev.hegel.Generators.sets;
import static dev.hegel.Generators.text;
Expand All @@ -43,6 +46,9 @@
import java.time.ZoneOffset;
import java.util.List;
import java.util.Objects;
import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.OptionalLong;
import java.util.regex.Pattern;
import org.junit.jupiter.api.Test;

Expand Down Expand Up @@ -170,6 +176,42 @@ void optionalBasicAndNonBasic() {
assertAllExamples(optional(integers().min(0).max(9).filter(x -> x > 4)), o -> o.isEmpty() || o.get() > 4);
}

// Each primitive optional stays within its inner generator's bounds, shrinks to empty (the
// simplest value), and when present shrinks to the inner generator's simplest value.

@Test
void optionalIntsBoundsAndShrinking() {
assertAllExamples(
optionalInts(integers().min(0).max(3)), o -> o.isEmpty() || (o.getAsInt() >= 0 && o.getAsInt() <= 3));
assertTrue(minimal(optionalInts(integers()), o -> true).isEmpty());
assertEquals(
5,
minimal(optionalInts(integers().min(5)), OptionalInt::isPresent).getAsInt());
}

@Test
void optionalLongsBoundsAndShrinking() {
assertAllExamples(
optionalLongs(longs().min(10L).max(20L)),
o -> o.isEmpty() || (o.getAsLong() >= 10L && o.getAsLong() <= 20L));
assertTrue(minimal(optionalLongs(longs()), o -> true).isEmpty());
assertEquals(
5L,
minimal(optionalLongs(longs().min(5L)), OptionalLong::isPresent).getAsLong());
}

@Test
void optionalDoublesBoundsAndShrinking() {
assertAllExamples(
optionalDoubles(doubles().min(0.0).max(1.0)),
o -> o.isEmpty() || (o.getAsDouble() >= 0.0 && o.getAsDouble() <= 1.0));
assertTrue(minimal(optionalDoubles(doubles()), o -> true).isEmpty());
assertEquals(
5.0,
minimal(optionalDoubles(doubles().min(5.0)), OptionalDouble::isPresent)
.getAsDouble());
}

@Test
void combinators() {
assertAllExamples(integers().min(0).max(50).map(x -> x * 2), x -> x % 2 == 0);
Expand Down
10 changes: 10 additions & 0 deletions shared/src/test/java/dev/hegel/DerivationTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.OptionalLong;
import java.util.Set;
import java.util.UUID;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -98,6 +101,13 @@ void derivesWrapperScalarsAndBytes() {
w -> w.l() != null && w.b() != null && w.d() != null && w.data() != null && w.i() != null);
}

record Stats(long count, OptionalInt min, OptionalLong max, OptionalDouble mean) {}

@Test
void derivesPrimitiveOptionals() {
assertAllExamples(forType(Stats.class), s -> s.min() != null && s.max() != null && s.mean() != null);
}

@Test
void derivesUuidScalars() {
assertAllExamples(forType(UUID.class), Objects::nonNull);
Expand Down
13 changes: 13 additions & 0 deletions shared/src/test/java/dev/hegel/GeneratorSmokeTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@
import static dev.hegel.Generators.fromRegex;
import static dev.hegel.Generators.integers;
import static dev.hegel.Generators.ipAddresses;
import static dev.hegel.Generators.longs;
import static dev.hegel.Generators.maps;
import static dev.hegel.Generators.oneOf;
import static dev.hegel.Generators.optional;
import static dev.hegel.Generators.optionalDoubles;
import static dev.hegel.Generators.optionalInts;
import static dev.hegel.Generators.optionalLongs;
import static dev.hegel.Generators.sampledFrom;
import static dev.hegel.Generators.sets;
import static dev.hegel.Generators.text;
Expand All @@ -26,6 +30,9 @@
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.OptionalLong;
import java.util.Set;

class GeneratorSmokeTest {
Expand Down Expand Up @@ -78,6 +85,12 @@ void tuplesAndOneOfAndOptional(TestCase tc) {
assertTrue(iv <= 1 || iv >= 100);
Optional<Integer> o = tc.draw(optional(integers().min(0).max(3)));
o.ifPresent(x -> assertTrue(x >= 0 && x <= 3));
OptionalInt oi = tc.draw(optionalInts(integers().min(0).max(3)));
oi.ifPresent(x -> assertTrue(x >= 0 && x <= 3));
OptionalLong ol = tc.draw(optionalLongs(longs().min(0L).max(3L)));
ol.ifPresent(x -> assertTrue(x >= 0L && x <= 3L));
OptionalDouble od = tc.draw(optionalDoubles(doubles().min(0.0).max(1.0)));
od.ifPresent(x -> assertTrue(x >= 0.0 && x <= 1.0));
}

@HegelTest
Expand Down
Loading