Skip to content

Commit

Permalink
Breaking change: stream() semantics when used with stateful generat…
Browse files Browse the repository at this point in the history
…ors.

With this commit, all root objects created by `stream()` are independent
of each other. This affects the behaviour of `stream()` when used with
stateful generators such as `emit()`, `intSeq()`, `longSeq()`.

Example:

```java
List<String> results = Instancio.of(String.class)
    .generate(allStrings(), gen -> gen.emit().items("foo", "bar", "baz").ignoreUnused())
    .stream()
    .limit(3)
    .toList();

// Original behaviour
assertThat(results).containsExactly("foo", "bar", "baz");

// New behaviour
assertThat(results).containsExactly("foo", "foo", "foo");
```
  • Loading branch information
armandino committed Dec 30, 2023
1 parent 1950f3e commit c58493d
Show file tree
Hide file tree
Showing 9 changed files with 396 additions and 18 deletions.
3 changes: 3 additions & 0 deletions instancio-core/src/main/java/org/instancio/InstancioApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,16 @@ public interface InstancioApi<T> extends
* <p>Example:
* <pre>{@code
* List<Person> persons = Instancio.of(Person.class)
* .generate(field(Person::getAge), gen -> gen.ints().range(18, 100))
* .stream()
* .limit(5)
* .collect(Collectors.toList());
* }</pre>
*
* <p><b>Warning:</b> {@code limit()} must be called to avoid an infinite loop.
*
* <p>All objects in the stream are independent of each other.
*
* @return an infinite stream of distinct object instances
* @since 1.1.9
*/
Expand Down
24 changes: 21 additions & 3 deletions instancio-core/src/main/java/org/instancio/internal/ApiImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.instancio.InstancioApi;
import org.instancio.Model;
import org.instancio.OnCompleteCallback;
import org.instancio.Random;
import org.instancio.Result;
import org.instancio.TargetSelector;
import org.instancio.TypeTokenSupplier;
Expand All @@ -30,6 +31,8 @@

import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Supplier;
import java.util.stream.Stream;

Expand Down Expand Up @@ -183,8 +186,24 @@ public Result<T> asResult() {

@Override
public Stream<T> stream() {
final InternalModel<T> model = createModel();
return Stream.generate(() -> createRootObject(model));
final AtomicBoolean modelDumped = new AtomicBoolean();
final AtomicLong nextSeed = new AtomicLong();

return Stream.generate(() -> {
final InternalModel<T> model = new InternalModel<>(modelContextBuilder.build());

// verbose() should print only once per stream()
if (modelDumped.compareAndSet(false, true)) {
InternalModelDump.printVerbose(model);
}

// Update seed for each stream element to avoid generating the same object
final Random random = model.getModelContext().getRandom();
nextSeed.set(random.longRange(1, Long.MAX_VALUE));

modelContextBuilder.withSeed(nextSeed.get());
return createRootObject(model);
});
}

private T createRootObject(final InternalModel<T> model) {
Expand All @@ -193,7 +212,6 @@ private T createRootObject(final InternalModel<T> model) {

private InternalModel<T> createModel() {
final InternalModel<T> model = new InternalModel<>(modelContextBuilder.build());
// should happen only once even when the result is a Stream
InternalModelDump.printVerbose(model);
return model;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,9 +175,7 @@ void shuffle() {

final List<Integer> result = Instancio.ofList(Integer.class)
.size(values.size())
.generate(all(Integer.class), gen -> gen.emit()
.items(values.toArray(new Integer[0]))
.shuffle())
.generate(all(Integer.class), gen -> gen.emit().items(values).shuffle())
.create();

assertThat(result)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright 2022-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.instancio.test.features.generator.misc;

import org.instancio.Instancio;
import org.instancio.Model;
import org.instancio.junit.InstancioExtension;
import org.instancio.test.support.tags.Feature;
import org.instancio.test.support.tags.FeatureTag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import static org.assertj.core.api.Assertions.assertThat;
import static org.instancio.Select.allStrings;

@FeatureTag({Feature.EMIT_GENERATOR, Feature.MODEL})
@ExtendWith(InstancioExtension.class)
class EmitModelTest {

@Test
void emitWithTwoModels() {
final String expected = "foo";

final Model<String> model = Instancio.of(String.class)
.generate(allStrings(), gen -> gen.emit().items(expected))
.toModel();

// Each root object should have its own copy of items to emit
// even though they share the same instance of a model
final String result1 = Instancio.create(model);
final String result2 = Instancio.create(model);

assertThat(result1).isEqualTo(result2).isEqualTo(expected);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright 2022-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.instancio.test.features.generator.misc;

import org.instancio.Instancio;
import org.instancio.TypeToken;
import org.instancio.exception.InstancioApiException;
import org.instancio.junit.InstancioExtension;
import org.instancio.test.support.tags.Feature;
import org.instancio.test.support.tags.FeatureTag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;

import static java.util.stream.Collectors.toList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.instancio.Select.all;

/**
* {@code stream()} creates a new root object each time.
* Therefore, each root object gets its own copy of values to emit.
*/
@FeatureTag({Feature.EMIT_GENERATOR, Feature.STREAM})
@ExtendWith(InstancioExtension.class)
class EmitStreamTest {

@Test
void throwsAnExceptionOnUnusedItems() {
final String[] items = {"foo", "bar", "baz"};

final Stream<String> stream = Instancio.of(String.class)
.generate(all(String.class), gen -> gen.emit().items(items))
.stream()
.limit(items.length);

assertThatThrownBy(() -> stream.collect(toList())) // NOSONAR
.isExactlyInstanceOf(InstancioApiException.class)
.hasMessageContainingAll(
"not all the items provided via the 'emit()' method have been consumed",
"Remaining items: [bar, baz]");
}

@Test
void ignoreUnusedItems() {
final String[] items = {"foo", "bar", "baz"};

final Stream<String> results = Instancio.of(String.class)
.generate(all(String.class), gen -> gen.emit().items(items).ignoreUnused())
.stream()
.limit(items.length);

assertThat(results)
.as("Should pick the first value from items for each root objectm and ignore the rest")
.containsExactly("foo", "foo", "foo");
}

@Test
void withMultipleItems() {
final String[] items = {"foo", "bar", "baz"};

final int outerListSize = 10;

final List<List<String>> results = Instancio.of(new TypeToken<List<String>>() {})
.generate(all(List.class), gen -> gen.collection().size(items.length)) // inner list
.generate(all(String.class), gen -> gen.emit().items(items))
.stream()
.limit(outerListSize)
.collect(toList());

assertThat(results)
.hasSize(outerListSize)
.allMatch(r -> r.equals(Arrays.asList(items)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
*/
package org.instancio.test.features.generator.sequence;

import org.instancio.Gen;
import org.instancio.Instancio;
import org.instancio.generator.specs.NumericSequenceSpec;
import org.instancio.generators.Generators;
import org.instancio.junit.InstancioExtension;
import org.instancio.test.support.pojo.basic.IntegerHolder;
Expand Down Expand Up @@ -74,6 +76,43 @@ void ofListAllInts() {

assertEachField123(result);
}
}

@Nested
class NumericSequenceWithStreamTest {

@Test
void streamWithGeneratorPerField() {
// Since the generator shared across all root objects created by stream(),
// the expected result is [1, 2, 3]
final NumericSequenceSpec<Integer> primitiveSeq = Gen.intSeq();
final NumericSequenceSpec<Integer> wrapperSeq = Gen.intSeq();

final List<IntegerHolder> result = Instancio.of(IntegerHolder.class)
.generate(field(IntegerHolder::getPrimitive), primitiveSeq)
.generate(field(IntegerHolder::getWrapper), wrapperSeq)
.stream()
.limit(3)
.collect(Collectors.toList());

assertEachField123(result);
}

@Test
void streamWithGeneratorSharedAcrossFields() {
// Shared by both fields
final NumericSequenceSpec<Integer> sharedSeq = Gen.intSeq();

final List<IntegerHolder> result = Instancio.of(IntegerHolder.class)
.generate(field(IntegerHolder::getPrimitive), sharedSeq)
.generate(field(IntegerHolder::getWrapper), sharedSeq)
.stream()
.limit(3)
.collect(Collectors.toList());

assertThat(result).extracting(IntegerHolder::getPrimitive).containsExactly(1, 3, 5);
assertThat(result).extracting(IntegerHolder::getWrapper).containsExactly(2, 4, 6);
}

@Test
void stream() {
Expand All @@ -84,13 +123,11 @@ void stream() {
.limit(3)
.collect(Collectors.toList());

assertEachField123(result);
// The sequence should start from 1 for each root object
assertThat(result).extracting(IntegerHolder::getPrimitive).containsExactly(1, 1, 1);
assertThat(result).extracting(IntegerHolder::getWrapper).containsExactly(1, 1, 1);
}

private void assertEachField123(final List<IntegerHolder> result) {
assertThat(result).extracting(IntegerHolder::getPrimitive).containsExactly(1, 2, 3);
assertThat(result).extracting(IntegerHolder::getWrapper).containsExactly(1, 2, 3);
}
}

@Test
Expand All @@ -114,4 +151,9 @@ void ofListWithPredicateSelector() {
assertThat(result).extracting(IntegerHolder::getPrimitive).containsExactly(1, 3, 5);
assertThat(result).extracting(IntegerHolder::getWrapper).containsExactly(2, 4, 6);
}

private static void assertEachField123(final List<IntegerHolder> result) {
assertThat(result).extracting(IntegerHolder::getPrimitive).containsExactly(1, 2, 3);
assertThat(result).extracting(IntegerHolder::getWrapper).containsExactly(1, 2, 3);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Stream;

import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
import static org.assertj.core.api.Assertions.assertThat;
import static org.instancio.Select.allStrings;
import static org.instancio.Select.field;
Expand Down Expand Up @@ -134,18 +134,19 @@ void ofTypeTokenStream() {
void withSeed() {
final long seed = Instancio.create(long.class);

final List<UUID> list1 = Instancio.of(UUID.class)
// Should produce distinct UUIDs
final Set<UUID> set1 = Instancio.of(UUID.class)
.withSeed(seed)
.stream()
.limit(LIMIT)
.collect(toList());
.collect(toSet());

final List<UUID> list2 = Instancio.of(UUID.class)
final Set<UUID> set2 = Instancio.of(UUID.class)
.withSeed(seed)
.stream()
.limit(LIMIT)
.collect(toList());
.collect(toSet());

assertThat(list1).isEqualTo(list2).hasSize(LIMIT);
assertThat(set1).isEqualTo(set2).hasSize(LIMIT);
}
}

0 comments on commit c58493d

Please sign in to comment.