DataProviderUtil.java

package net.morimekta.testing.junit4;

import java.util.ArrayList;
import java.util.List;

/**
 * Utility for using <code>junit-dataprovider</code>.
 */
public class DataProviderUtil {
    /**
     * This is a utility to be used with <code>junit-dataprovider</code>. It will instead of taking in an array of
     * argument arrays, will get an array of available dimensions, each with possible values. It will then multiply all
     * the possible dimension possibilities with each other and result the array of parameter array that
     * <code>junit-dataprovider</code> wants.
     * <p>
     * E.g. providing two dimensions, content type and locale, e.g. like this:
     *
     * <pre>{@code
     * {@literal@}RunWith(DataProviderRunner.class)
     * public class MyTest {
     *     {@literal@}DataProvider
     *     public static Object[][] testParams() {
     *         return buildDataDimensions(
     *             List.of(ContentType.WALLPAPER, ContentType.AUDIO),
     *             List.of(Locale.US, Locale.DE, Locale.SIMPLIFIED_CHINESE));
     *     }
     *     {@literal@}Test
     *     {@literal@}UseDataProvider("testParams")
     *     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 new Object[][]{
     *     {ContentType.WALLPAPER, Locale.US},
     *     {ContentType.WALLPAPER, Locale.DE},
     *     {ContentType.WALLPAPER, Locale.SIMPLIFIED_CHINESE},
     *     {ContentType.AUDIO, Locale.US},
     *     {ContentType.AUDIO, Locale.DE},
     *     {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 Object[][] buildDataDimensions(List<?>... dimensions) {
        if (dimensions.length == 0) {
            throw new IllegalArgumentException("No dimensions provided");
        }
        if (dimensions[0].size() == 0) {
            throw new IllegalArgumentException("Empty dimension in layer 1");
        }

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

        for (int layer = 1; layer < dimensions.length; ++layer) {
            if (dimensions[layer].size() == 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 < dimensions[layer].size(); ++pos) {
                List<List<Object>> extraResults = deepCopy(layerBelow);
                for (List<Object> l : extraResults) {
                    l.add(dimensions[layer].get(pos));
                }
                result.addAll(extraResults);
            }
        }

        return makeDataParams(result);
    }

    /**
     * Convenience method to be able to use lists instead of arrays to build the dimensions. This must be a a list of
     * lists of equal sizes, with compatible argument objects in each list.
     *
     * @param lists List of list of arguments.
     * @return Array of array of arguments.
     */
    public static Object[][] makeDataParams(List<List<Object>> lists) {
        // assumes all inner lists are the same size.
        Object[][] out = new Object[lists.size()][];
        for (int i = 0; i < lists.size(); ++i) {
            out[i] = lists.get(i).toArray(EMPTY);
        }
        return out;
    }

    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 final Object[] EMPTY = new Object[0];

    private DataProviderUtil() {
    }
}