Skip to content

Commit b668b35

Browse files
authored
ParameterizedTest utilities (#3336)
This adds a couple utilities to our testing frameworks: 1. An optional name for `@BooleanSource`, so that it doesn't just appear as `true` / `false` 2. Methods for producing similar named boolean streams for use in a `@MethodSource` 3. A method to produce the cartesian product of a bunch of streams, instead of having to chain a whole bunch of `flatMap`s. Below is a screenshot of the test results after this change, for `FDBRecordStoreUniqueIndexTest` that takes full advantage of the new `@BooleanSource` functionality ![image](https://github.com/user-attachments/assets/a6e168a5-f7ae-4759-90fa-0fd5b60073e8) vs the old way: ![image](https://github.com/user-attachments/assets/2d33456d-eb4e-4077-87f1-dc3e12fe3f01) vs if you don't have the `name=` on all the tests ![image](https://github.com/user-attachments/assets/079d6d8d-7516-48c6-aa92-673a5b2eb950) (Note: `removeUniquenessConstraintDuringTransactionWithNewDuplications` was taking `removeUniquenessConstraintArguments` but only actually used one of the parameters, causing it to run twice as many times as needed)
1 parent 4578dab commit b668b35

File tree

8 files changed

+213
-44
lines changed

8 files changed

+213
-44
lines changed

fdb-extensions/src/test/java/com/apple/foundationdb/async/MoreAsyncUtilTest.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
package com.apple.foundationdb.async;
2222

2323
import com.apple.foundationdb.test.TestExecutors;
24+
import com.apple.test.ParameterizedTestUtils;
2425
import com.google.common.util.concurrent.ThreadFactoryBuilder;
2526
import org.hamcrest.Matcher;
2627
import org.junit.jupiter.api.Test;
@@ -215,10 +216,10 @@ enum FutureBehavior {
215216
}
216217

217218
public static Stream<Arguments> combineAndFailFast() {
218-
return Arrays.stream(FutureBehavior.values())
219-
.flatMap(future1 ->
220-
Arrays.stream(FutureBehavior.values())
221-
.map(future2 -> Arguments.of(future1, future2)));
219+
return ParameterizedTestUtils.cartesianProduct(
220+
Arrays.stream(FutureBehavior.values()),
221+
Arrays.stream(FutureBehavior.values())
222+
);
222223
}
223224

224225
@ParameterizedTest

fdb-extensions/src/test/java/com/apple/test/BooleanArgumentsProvider.java

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,25 +25,29 @@
2525
import org.junit.jupiter.params.provider.ArgumentsProvider;
2626
import org.junit.jupiter.params.support.AnnotationConsumer;
2727

28-
import javax.annotation.Nonnull;
2928
import java.util.Arrays;
30-
import java.util.List;
3129
import java.util.stream.Stream;
3230

3331
/**
3432
* Argument provider for the {@link BooleanSource} annotation for providing booleans to parameterized
35-
* tests. Regardless of the source or context, this always returns {@code false} and {@code true} in that order.
33+
* tests. Regardless of the source or context, this always returns {@code false} and {@code true} in that order for each
34+
* argument.
3635
*/
3736
class BooleanArgumentsProvider implements ArgumentsProvider, AnnotationConsumer<BooleanSource> {
38-
@Nonnull
39-
private static final List<Arguments> BOOLEANS = Arrays.asList(Arguments.of(false), Arguments.of(true));
37+
private String[] names;
4038

4139
@Override
4240
public void accept(BooleanSource booleanSource) {
41+
this.names = booleanSource.value();
4342
}
4443

4544
@Override
4645
public Stream<? extends Arguments> provideArguments(ExtensionContext extensionContext) throws Exception {
47-
return BOOLEANS.stream();
46+
if (names.length == 0) {
47+
throw new IllegalStateException("@BooleanSource has an empty list of names");
48+
}
49+
return ParameterizedTestUtils.cartesianProduct(Arrays.stream(names)
50+
.map(ParameterizedTestUtils::booleans)
51+
.toArray(Stream[]::new));
4852
}
4953
}

fdb-extensions/src/test/java/com/apple/test/BooleanSource.java

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,24 @@
2929
import java.lang.annotation.Target;
3030

3131
/**
32-
* An annotation for parameterized tests that take a single boolean argument. One might think one could use
33-
* {@link org.junit.jupiter.params.provider.ValueSource} for that, but that annotation does not allow
34-
* one to set a boolean value. This will always provide {@code false} and {@code true} as the single argument
35-
* to parameterized tests that have this annotation in that order.
32+
* An annotation for parameterized tests that take boolean arguments.
33+
* One could use {@link org.junit.jupiter.params.provider.ValueSource} for that, but it requires you to explicitly state
34+
* that you want to run for {@code true} and {@code false}.
35+
* By default, this will provide {@code false} and {@code true} as the single argument to parameterized tests that have
36+
* this annotation in that order.
37+
* If more than one {@code value} is provided, this will produce the same number of boolean arguments, each with the
38+
* given name.
3639
*/
3740
@Documented
3841
@Target(ElementType.METHOD)
3942
@Retention(RetentionPolicy.RUNTIME)
4043
@ArgumentsSource(BooleanArgumentsProvider.class)
4144
public @interface BooleanSource {
45+
/**
46+
* A name to give the boolean values; if unspecified, {@code "true"} and {@code "false"} will be used.
47+
* If multiple values are given, the cartesian product of {@code true} and {@code false} will be used for each of
48+
* the names, giving all combinations. There should be one value in this array per boolean parameter to the test.
49+
* @return the names to be used for the true value for each argument, the false value will be prefixed with "!"
50+
*/
51+
String[] value() default "";
4252
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* BooleanArguments.java
3+
*
4+
* This source file is part of the FoundationDB open source project
5+
*
6+
* Copyright 2015-2025 Apple Inc. and the FoundationDB project authors
7+
*
8+
* Licensed under the Apache License, Version 2.0 (the "License");
9+
* you may not use this file except in compliance with the License.
10+
* You may obtain a copy of the License at
11+
*
12+
* http://www.apache.org/licenses/LICENSE-2.0
13+
*
14+
* Unless required by applicable law or agreed to in writing, software
15+
* distributed under the License is distributed on an "AS IS" BASIS,
16+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
* See the License for the specific language governing permissions and
18+
* limitations under the License.
19+
*/
20+
21+
package com.apple.test;
22+
23+
import org.junit.jupiter.api.Named;
24+
import org.junit.jupiter.params.provider.Arguments;
25+
26+
import java.util.List;
27+
import java.util.stream.Collectors;
28+
import java.util.stream.Stream;
29+
30+
/**
31+
* Helper utility class for interacting with {@link org.junit.jupiter.params.ParameterizedTest}s.
32+
*/
33+
public class ParameterizedTestUtils {
34+
35+
/**
36+
* Provides a stream of boolean, named arguments.
37+
* @param trueName the name to provide for {@code true}
38+
* @param falseName the name to provide for {@code false}
39+
* @return a stream to be used as a return value for a {@link org.junit.jupiter.params.provider.MethodSource}
40+
*/
41+
public static Stream<Named<Boolean>> booleans(String trueName, String falseName) {
42+
return Stream.of(Named.of(falseName, false), Named.of(trueName, true));
43+
}
44+
45+
/**
46+
* Provides a stream of boolean, named arguments.
47+
* @param name the name to provide for {@code true}, {@code false} will prefix with {@code "!"}. If blank,
48+
* {@code "true"} and {@code "false"} will be used respectively.
49+
* @return a stream to be used as a return value for a {@link org.junit.jupiter.params.provider.MethodSource}
50+
*/
51+
public static Stream<Named<Boolean>> booleans(String name) {
52+
if (name.isBlank()) {
53+
return booleans("true", "false");
54+
} else {
55+
return booleans(name, "!" + name);
56+
}
57+
}
58+
59+
/**
60+
* Produce the cartesian product of the given streams as {@link Arguments}.
61+
* @param sources a list of sources to combine.
62+
* @return a stream of {@link Arguments} where the 0th element is from the first source, the 1st is from the first,
63+
* and so-on. Producing all combinations of an element from each source.
64+
*/
65+
public static Stream<Arguments> cartesianProduct(Stream<?>... sources) {
66+
return cartesianProduct(
67+
Stream.of(sources)
68+
.map(stream -> stream.collect(Collectors.toList())).collect(Collectors.toList()))
69+
.map(arguments -> Arguments.of(arguments.toArray()));
70+
}
71+
72+
private static Stream<Stream<Object>> cartesianProduct(List<List<?>> sources) {
73+
if (sources.isEmpty()) {
74+
return Stream.of();
75+
}
76+
if (sources.size() == 1) {
77+
return sources.get(0).stream().map(Stream::of);
78+
}
79+
return sources.get(0).stream().flatMap(arg ->
80+
cartesianProduct(sources.subList(1, sources.size()))
81+
.map(recursed -> Stream.concat(Stream.of(arg), recursed)));
82+
}
83+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* ParameterizedTestUtilsTest.java
3+
*
4+
* This source file is part of the FoundationDB open source project
5+
*
6+
* Copyright 2015-2025 Apple Inc. and the FoundationDB project authors
7+
*
8+
* Licensed under the Apache License, Version 2.0 (the "License");
9+
* you may not use this file except in compliance with the License.
10+
* You may obtain a copy of the License at
11+
*
12+
* http://www.apache.org/licenses/LICENSE-2.0
13+
*
14+
* Unless required by applicable law or agreed to in writing, software
15+
* distributed under the License is distributed on an "AS IS" BASIS,
16+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
* See the License for the specific language governing permissions and
18+
* limitations under the License.
19+
*/
20+
21+
package com.apple.test;
22+
23+
import org.junit.jupiter.api.Named;
24+
import org.junit.jupiter.api.Test;
25+
26+
import java.util.List;
27+
import java.util.function.Supplier;
28+
import java.util.stream.Collectors;
29+
import java.util.stream.Stream;
30+
31+
import static org.junit.jupiter.api.Assertions.assertEquals;
32+
33+
public class ParameterizedTestUtilsTest {
34+
35+
@Test
36+
void cartesianProduct1() {
37+
final Supplier<Stream<Integer>> integers = () -> Stream.of(1, 2, 3);
38+
assertEquals(integers.get().map(List::of).collect(Collectors.toList()),
39+
ParameterizedTestUtils.cartesianProduct(integers.get())
40+
.map(args -> List.of(args.get())).collect(Collectors.toList()));
41+
}
42+
43+
@Test
44+
void cartesianProduct2() {
45+
final Supplier<Stream<Integer>> integers = () -> Stream.of(1, 2, 3);
46+
final Supplier<Stream<Boolean>> booleans = () -> Stream.of(true, false);
47+
assertEquals(
48+
integers.get().flatMap(i ->
49+
booleans.get().map(b -> List.of(i, b)))
50+
.collect(Collectors.toList()),
51+
ParameterizedTestUtils.cartesianProduct(
52+
integers.get(),
53+
booleans.get()
54+
).map(args -> List.of(args.get())).collect(Collectors.toList()));
55+
}
56+
57+
@Test
58+
void cartesianProduct5() {
59+
final List<Integer> integers = List.of(1, 2, 3);
60+
final List<Boolean> booleans = List.of(true, false);
61+
final List<String> strings = List.of("foo", "bar", "something", "or other");
62+
final List<Named<Supplier<String>>> namedThings = List.of(Named.of("banana", () -> "banana"),
63+
Named.of("apple", () -> "apple"));
64+
assertEquals(
65+
integers.stream().flatMap(i ->
66+
booleans.stream().flatMap(b ->
67+
strings.stream().flatMap(s ->
68+
namedThings.stream().map(named ->
69+
List.of(i, b, s, named, 4.2)))))
70+
.collect(Collectors.toList()),
71+
ParameterizedTestUtils.cartesianProduct(
72+
integers.stream(),
73+
booleans.stream(),
74+
strings.stream(),
75+
namedThings.stream(),
76+
Stream.of(4.2)
77+
).map(args -> List.of(args.get())).collect(Collectors.toList()));
78+
}
79+
}

fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/APIVersionTest.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
package com.apple.foundationdb.record.provider.foundationdb;
2222

23+
import com.apple.test.ParameterizedTestUtils;
2324
import org.junit.jupiter.params.ParameterizedTest;
2425
import org.junit.jupiter.params.provider.Arguments;
2526
import org.junit.jupiter.params.provider.MethodSource;
@@ -34,13 +35,13 @@ class APIVersionTest {
3435

3536
@SuppressWarnings("unused") // used as argument source for parameterized test
3637
static Stream<Arguments> isAtLeast() {
37-
return Arrays.stream(APIVersion.values())
38-
.flatMap(version1 -> Arrays.stream(APIVersion.values())
39-
.map(version2 -> Arguments.of(version1, version2))
40-
);
38+
return ParameterizedTestUtils.cartesianProduct(
39+
Arrays.stream(APIVersion.values()),
40+
Arrays.stream(APIVersion.values())
41+
);
4142
}
4243

43-
@ParameterizedTest(name = "isAtLeast[{arguments}]")
44+
@ParameterizedTest
4445
@MethodSource
4546
void isAtLeast(@Nonnull APIVersion version1, @Nonnull APIVersion version2) {
4647
assertEquals(version1.getVersionNumber() >= version2.getVersionNumber(), version1.isAtLeast(version2));

fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/FDBRecordStoreUniqueIndexTest.java

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,6 @@
6262
import org.junit.jupiter.api.Tag;
6363
import org.junit.jupiter.api.Test;
6464
import org.junit.jupiter.params.ParameterizedTest;
65-
import org.junit.jupiter.params.provider.Arguments;
66-
import org.junit.jupiter.params.provider.MethodSource;
6765

6866
import javax.annotation.Nonnull;
6967
import javax.annotation.Nullable;
@@ -81,7 +79,6 @@
8179
import java.util.concurrent.atomic.AtomicBoolean;
8280
import java.util.concurrent.atomic.AtomicReference;
8381
import java.util.stream.Collectors;
84-
import java.util.stream.Stream;
8582

8683
import static com.apple.foundationdb.record.metadata.Key.Expressions.field;
8784
import static org.hamcrest.MatcherAssert.assertThat;
@@ -233,12 +230,6 @@ public void buildUniqueInCheckVersion() throws Exception {
233230
}
234231
}
235232

236-
static Stream<Arguments> removeUniquenessConstraintArguments() {
237-
return Stream.of(true, false)
238-
.flatMap(withViolations -> Stream.of(true, false)
239-
.map(allowReadableUniquePending -> Arguments.of(withViolations, allowReadableUniquePending)));
240-
}
241-
242233
@Test
243234
public void uniquenessChecks() throws Exception {
244235
try (FDBRecordContext context = openContext()) {
@@ -462,8 +453,8 @@ public void removeUniquenessConstraint() throws Exception {
462453
dropUniquenessConstraint.openWithNonUnique(false, true);
463454
}
464455

465-
@ParameterizedTest(name = "removeUniquenessConstraintAfterBuild(withViolations={0}, allowReadableUniquePending={1})")
466-
@MethodSource("removeUniquenessConstraintArguments")
456+
@ParameterizedTest
457+
@BooleanSource({"withViolations", "allowReadableUniquePending"})
467458
void removeUniquenessConstraintAfterBuild(boolean withViolations, boolean allowReadableUniquePending) throws Exception {
468459
final DropUniquenessConstraint dropUniquenessConstraint = new DropUniquenessConstraint(allowReadableUniquePending);
469460
dropUniquenessConstraint.setupStore();
@@ -474,8 +465,8 @@ void removeUniquenessConstraintAfterBuild(boolean withViolations, boolean allowR
474465
dropUniquenessConstraint.openWithNonUnique(false, true);
475466
}
476467

477-
@ParameterizedTest(name = "removeUniquenessConstraintDuringTransaction(clearViolations={0}, allowReadableUniquePending={1})")
478-
@MethodSource("removeUniquenessConstraintArguments")
468+
@ParameterizedTest
469+
@BooleanSource({"clearViolations", "allowReadableUniquePending"})
479470
void removeUniquenessConstraintDuringTransaction(boolean clearViolations, boolean allowReadableUniquePending) throws Exception {
480471
final DropUniquenessConstraint dropUniquenessConstraint = new DropUniquenessConstraint(allowReadableUniquePending);
481472
dropUniquenessConstraint.setupStore();
@@ -489,8 +480,8 @@ void removeUniquenessConstraintDuringTransaction(boolean clearViolations, boolea
489480
* @param allowReadableUniquePending whether to allow {@link IndexState#READABLE_UNIQUE_PENDING}
490481
* @throws Exception if there is an issue
491482
*/
492-
@ParameterizedTest(name = "removeUniquenessConstraintDuringTransactionWithNewDuplications(addViolations={0}, allowReadableUniquePending={1})")
493-
@MethodSource("removeUniquenessConstraintArguments")
483+
@ParameterizedTest
484+
@BooleanSource("allowReadableUniquePending")
494485
void removeUniquenessConstraintDuringTransactionWithNewDuplications(boolean allowReadableUniquePending) throws Exception {
495486
final DropUniquenessConstraint dropUniquenessConstraint = new DropUniquenessConstraint(allowReadableUniquePending);
496487
dropUniquenessConstraint.setupStore();
@@ -509,8 +500,8 @@ void removeUniquenessConstraintDuringTransactionWithNewDuplications(boolean allo
509500
* @param allowReadableUniquePending whether to allow {@link IndexState#READABLE_UNIQUE_PENDING}
510501
* @throws Exception if there is an issue
511502
*/
512-
@ParameterizedTest(name = "removeUniquenessConstraintDuringTransactionWithNewViolations(allowReadableUniquePending={0})")
513-
@BooleanSource
503+
@ParameterizedTest
504+
@BooleanSource("allowReadableUniquePending")
514505
void removeUniquenessConstraintDuringTransactionWithNewViolations(boolean allowReadableUniquePending) throws Exception {
515506
final DropUniquenessConstraint dropUniquenessConstraint = new DropUniquenessConstraint(allowReadableUniquePending);
516507
dropUniquenessConstraint.setupStore();
@@ -524,8 +515,8 @@ void removeUniquenessConstraintDuringTransactionWithNewViolations(boolean allowR
524515
* still works.
525516
* @param allowReadableUniquePending whether to allow {@link IndexState#READABLE_UNIQUE_PENDING}
526517
*/
527-
@ParameterizedTest(name = "bumpVersionWhenChangingToNonUnique(allowReadableUniquePending={0})")
528-
@BooleanSource
518+
@ParameterizedTest
519+
@BooleanSource("allowReadableUniquePending")
529520
void bumpVersionWhenChangingToNonUnique(boolean allowReadableUniquePending) throws Exception {
530521
final DropUniquenessConstraint dropUniquenessConstraint = new DropUniquenessConstraint(
531522
allowReadableUniquePending, NO_UNIQUE_CLEAR_INDEX_TYPE, true);
@@ -540,8 +531,8 @@ void bumpVersionWhenChangingToNonUnique(boolean allowReadableUniquePending) thro
540531
* instructions on {@link IndexOptions#UNIQUE_OPTION}.
541532
* @param allowReadableUniquePending whether to test with allowing {@link IndexState#READABLE_UNIQUE_PENDING}
542533
*/
543-
@ParameterizedTest(name = "unsupportedChangeToNonUniqueWithoutBumpingVersion(allowReadableUniquePending={0})")
544-
@BooleanSource
534+
@ParameterizedTest
535+
@BooleanSource("allowReadableUniquePending")
545536
void unsupportedChangeToNonUniqueWithoutBumpingVersion(boolean allowReadableUniquePending) throws Exception {
546537
// this won't fail, it will just leave the violations sitting around, but as per the
547538
final DropUniquenessConstraint dropUniquenessConstraint = new DropUniquenessConstraint(

fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/runners/TransactionalRunnerTest.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -454,8 +454,8 @@ void runSynchronously() {
454454
}
455455
}
456456

457-
@ParameterizedTest(name = "closesAfterCompletion({argumentsWithNames})")
458-
@BooleanSource
457+
@ParameterizedTest
458+
@BooleanSource("successful")
459459
void closesAfterCompletion(boolean success) {
460460
AtomicReference<FDBRecordContext> contextRef = new AtomicReference<>();
461461
try (TransactionalRunner runner = defaultTransactionalRunner()) {
@@ -489,8 +489,8 @@ void closesAfterCompletion(boolean success) {
489489
}
490490
}
491491

492-
@ParameterizedTest(name = "closesAfterCompletionSynchronous({argumentsWithNames})")
493-
@BooleanSource
492+
@ParameterizedTest
493+
@BooleanSource("successful")
494494
void closesAfterCompletionSynchronous(boolean success) throws Exception {
495495
AtomicReference<FDBRecordContext> contextRef = new AtomicReference<>();
496496
try (TransactionalRunner runner = defaultTransactionalRunner()) {

0 commit comments

Comments
 (0)