Primitives and Determinism
Primitives and Determinism
Primitive generators
int n = Generators.ofInt(10, 99).generate();
double p = Generators.ofDouble(0.0, 1.0).generate();
String token = Generators.ofString(
StringGenerator.builder()
.length(16)
.charGenerator(CharGenerator.alphanumeric())
).generate();
Reproducible sequences
Build generators from a seeded GeneratorConfig. Each generator created from the same seeded
configuration starts from the same random state:
GeneratorConfig seeded = GeneratorConfig.builder().seed(123L).build();
IntGenerator a = Generators.ofInt(1, 100, seeded);
IntGenerator b = Generators.ofInt(1, 100, seeded);
assert a.generate() == b.generate();
assert a.generate() == b.generate();
Generator instances hold mutable random state. Treat them as per-test or per-thread objects, not shared
singletons. Share GeneratorConfig when you want common defaults, then create fresh generators from it
inside each test, fixture factory, or worker thread.
Composition
Generator.map(...) transforms values lazily, and Generator.filter(...) retries until a generated value matches.
Filters are bounded by default, so an impossible predicate fails instead of hanging a test run. Raise or lower the cap
with filter(predicate, maxAttempts) when the predicate is deliberately rare:
Generator<String> evenLabel = Generators.ofInt(0, 100)
.filter(n -> n % 2 == 0)
.map(n -> "even-" + n);
Generator<Integer> rare = Generators.ofInt(0, 1_000_000)
.filter(n -> n == 42, 100_000);
Selection helpers
List<String> colors = List.of("red", "green", "blue");
String one = Generators.pick(colors).generate();
List<String> two = Generators.pickSet(colors, 2).generate();
List<String> shuffled = Generators.shuffle(colors).generate();
Explicit replay snapshots (2.3+)
A seed does not freeze time. For temporal fixtures, capture the clock before generation and use that same configuration for the recipe printed on failure:
GeneratorConfig session = GeneratorConfig.builder().seed(42L).build().snapshotClock();
DateGenerator dates = new DateGenerator(session);
LocalDate fixture = dates.future(7);
String recipe = session.getGenerationRecipe().orElseThrow().serializeForDiagnostics();
snapshotClock() captures the configured instant and zone. The original configuration keeps its
live clock. Reuse the snapshot across the generation session; taking a new snapshot after failure
cannot recover an earlier clock. Custom callbacks and registries still do not yield a portable
recipe. Changes made to an injected builder must be included in the actual configuration you use
for generation and diagnostics.
Independent object-field streams (2.3+)
GeneratorConfig config = GeneratorConfig.builder()
.seed(42L)
.objectFieldStreamPolicy(ObjectFieldStreamPolicy.INDEPENDENT)
.build()
.snapshotClock();
ObjectFieldStreamPolicy is in io.github.frikit.krandom.generator.object. INDEPENDENT retains
named streams for seed-owned object members when custom rules, exclusions, or modules are
installed. An override of one field then leaves unrelated structural fields on their existing
streams. A numeric or textual seed is required. Dependent rules and semantic relationships may
intentionally change related values; arbitrary callbacks remain responsible for their own state.
The default LEGACY policy preserves 2.2 output and the existing portability boundary. The random
algorithm and child-seed derivation are unchanged. Portable recipes with the explicit policy include
object.field-stream-policy; absent settings retain legacy interpretation. Older readers reject
that new setting rather than silently changing replay. Customized configurations still return no
portable recipe. Roll back by selecting LEGACY or removing the option and regenerating the recipe.
Independent streams can cost more for customized fixtures because each member receives its own resolver stream. In the 2.3.0 qualification’s three-member structural record, customized throughput was about 32k objects/sec with INDEPENDENT versus 101k with LEGACY, with 3.4 times the allocations. Existing default throughput stayed within 1% of 2.2.0. These measurements are workload-specific; choose stability when it matters and benchmark your own fixture before bulk-generation adoption.
Composite child streams
Composite value generators such as PaymentInfoGenerator, OrderInfoGenerator, or
PersonInfoGenerator build their child generators from the same configuration. With a seed, every
child’s random source starts at the same state, so sibling values can be correlated — for example
the payment, invoice, and order identifiers of one seeded PaymentInfo share the same suffix. The
default ChildStreamPolicy.LEGACY keeps that published output unchanged.
GeneratorConfig config = GeneratorConfig.builder()
.seed(42L)
.childStreamPolicy(ChildStreamPolicy.INDEPENDENT)
.build();
With INDEPENDENT, each child receives config.forChildStream(name): a copy whose seed is
GenerationRecipe.deriveChildSeed(seed, name) for a stable per-child stream name, so siblings draw
from independent streams while the output stays reproducible. A numeric or textual seed is
required. Portable recipes record the explicit policy as child-stream-policy; an absent setting
means LEGACY. Custom composites can adopt the same contract by passing
config.forChildStream("childName") to each child they construct.
The same applies to separate generators built from one seeded configuration: each starts from the
root seed, so two Generators.ofInt(1, 40, config) instances return the same values, and
generators of different types draw from the same random sequence. Give each generator its own
stream by passing config.forChildStream(name) under INDEPENDENT:
GeneratorConfig config = GeneratorConfig.builder()
.seed("checkout-fixtures")
.childStreamPolicy(ChildStreamPolicy.INDEPENDENT)
.build();
Generator<String> firstNames = Generators.ofFirstName(config.forChildStream("firstName"));
Generator<String> lastNames = Generators.ofLastName(config.forChildStream("lastName"));
Under the default LEGACY policy forChildStream returns the configuration unchanged, so it has no
effect there. Krandom does this for you: each of its methods has a stream derived from the seed
and the method’s name, whatever the child-stream policy.