ParamsProviderUtil.java

package net.morimekta.testing.junit5;

import org.junit.jupiter.params.provider.Arguments;

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

/**
 * Utility class for building argument stream for {@link org.junit.jupiter.params.provider.MethodSource}.
 */
public final class ParamsProviderUtil {
    /**
     * This is a utility to be used with <code>junit-jupiter-params</code> argument expander. It will instead of just
     * returning the argument stream provided, will take a set of arguments, each with a specific type, and return the
     * cross product of these argument lists.
     * <p>
     * E.g. providing two dimensions, content type and locale, e.g. like this:
     *
     * <pre>{@code
     * public class MyTest {
     *     public static Stream&lt;Arguments&gt; myParamsSource() {
     *         return buildArgumentDimensions(
     *             arguments(ContentType.WALLPAPER, ContentType.AUDIO),
     *             arguments(Locale.US, Locale.DE, Locale.SIMPLIFIED_CHINESE));
     *     }
     *     {@literal@}ParameterizedTest
     *     {@literal@}MethodSource("myParamsSource")
     *     public void testMyParams(ContentType contentType, Locale locale) {
     *         // The test.
     *     }
     * }
     * }</pre>
     * <p>
     * Will create 6 argument arrays, one for each combination of content type and locale as if the input was this:
     *
     * <pre>{@code
     * return Stream.of(
     *     arguments(ContentType.WALLPAPER, Locale.US),
     *     arguments(ContentType.WALLPAPER, Locale.DE),
     *     arguments(ContentType.WALLPAPER, Locale.SIMPLIFIED_CHINESE),
     *     arguments(ContentType.AUDIO, Locale.US),
     *     arguments(ContentType.AUDIO, Locale.DE),
     *     arguments(ContentType.AUDIO, Locale.SIMPLIFIED_CHINESE),
     * });
     * }</pre>
     * <p>
     * This can significantly shorten the argument list especially with larger set of dimensions and longer lists of
     * options for each. It accepts null values and does not accept empty dimensions. Each dimension is called a 'layer'
     * in the input.
     *
     * @param dimensions The available dimensions.
     * @return The argument arrays.
     */
    public static Stream<Arguments> buildArgumentDimensions(Arguments... dimensions) {
        if (dimensions.length == 0) {
            throw new IllegalArgumentException("No dimensions provided");
        }
        if (dimensions[0].get().length == 0) {
            throw new IllegalArgumentException("Empty dimension in layer 1");
        }

        List<List<Object>> result = new ArrayList<>();
        for (Object o : dimensions[0].get()) {
            List<Object> base = new ArrayList<>();
            base.add(o);
            result.add(base);
        }

        for (int layer = 1; layer < dimensions.length; ++layer) {
            final int layerSize = dimensions[layer].get().length;
            if (layerSize == 0) {
                throw new IllegalArgumentException("Empty dimension in layer " + (layer + 1));
            }

            // The layer below is the one that will be multiplied with the
            // arguments from this layer.
            List<List<Object>> layerBelow = deepCopy(result);

            Object first = dimensions[layer].get()[0];
            for (List<Object> l : result) {
                l.add(first);
            }

            for (int pos = 1; pos < layerSize; ++pos) {
                List<List<Object>> extraResults = deepCopy(layerBelow);
                for (List<Object> l : extraResults) {
                    l.add(dimensions[layer].get()[pos]);
                }
                result.addAll(extraResults);
            }
        }

        return fromLists(result);
    }

    private static List<List<Object>> deepCopy(List<List<Object>> base) {
        List<List<Object>> copy = new ArrayList<>(base.size());
        for (List<Object> list : base) {
            copy.add(new ArrayList<>(list));
        }
        return copy;
    }

    private static Stream<Arguments> fromLists(List<List<Object>> lists) {
        // assumes all inner lists are the same size.
        List<Arguments> out = new ArrayList<>(lists.size());
        for (List<Object> list : lists) {
            out.add(Arguments.arguments(list.toArray(EMPTY)));
        }
        return out.stream();
    }

    private static final Object[] EMPTY = new Object[0];

    private ParamsProviderUtil() {
    }
}