Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Breaking change: stream() semantics when used with stateful generators #869

Merged
merged 1 commit into from
Dec 30, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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);
}
}