001/**
002 * The MIT License (MIT)
003 *
004 * Copyright (c) 2017 tools4j.org (Marco Terzer)
005 *
006 * Permission is hereby granted, free of charge, to any person obtaining a copy
007 * of this software and associated documentation files (the "Software"), to deal
008 * in the Software without restriction, including without limitation the rights
009 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
010 * copies of the Software, and to permit persons to whom the Software is
011 * furnished to do so, subject to the following conditions:
012 *
013 * The above copyright notice and this permission notice shall be included in all
014 * copies or substantial portions of the Software.
015 *
016 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
017 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
018 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
019 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
020 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
021 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
022 * SOFTWARE.
023 */
024package org.tools4j.spockito;
025
026import org.junit.Assert;
027import org.junit.Test;
028import org.junit.runner.Runner;
029import org.junit.runner.manipulation.Filter;
030import org.junit.runner.manipulation.NoTestsRemainException;
031import org.junit.runners.Suite;
032import org.junit.runners.model.FrameworkMethod;
033import org.junit.runners.model.InitializationError;
034import org.junit.runners.model.TestClass;
035
036import java.lang.annotation.*;
037import java.lang.reflect.Constructor;
038import java.lang.reflect.Executable;
039import java.lang.reflect.Parameter;
040import java.text.MessageFormat;
041import java.util.ArrayList;
042import java.util.List;
043
044/**
045 * The custom runner <code>Spockito</code> implements parameterized tests where the test data
046 * is defined in a table-like structure via {@link Unroll} annotation.
047 */
048public class Spockito extends Suite {
049
050    private static final String DEFAULT_NAME = "[{row}]: {0}";
051
052    /**
053     * Annotation for a test or a test method which provides test data for the tests. If the
054     * annotation made at the test class level, then the test data is applied to all test
055     * methods. Test data on method level is applied for that method only.
056     */
057    @Retention(RetentionPolicy.RUNTIME)
058    @Target(value = {ElementType.TYPE, ElementType.METHOD})
059    public @interface Unroll {
060        /**
061         * Annotation with test case values declared in a table like structure as follows:
062         * <pre>
063         * | ColumnA   | ColumnB   | ColumnC   |
064         * |-----------|-----------|-----------|
065         * | value_1_A | value_1_B | value_1_C |
066         * | value_2_A | value_2_B | value_2_C |
067         * etc...
068         * </pre>
069         * The separator row after the column headers is optional and = instead of - can be used. Separator rows can be
070         * placed anywhere in the table and are ignored when the table is parsed.
071         *
072         * @return  An array of strings represented as table data; string[0] contains the header
073         *          row with column names
074         * @see MessageFormat
075         */
076        String[] value();
077    }
078
079    /**
080     * Annotation for a test or a test method to indicate an alternative name for the parameterized
081     * test. Default name is "[{row}]: {0}".
082     */
083    @Retention(RetentionPolicy.RUNTIME)
084    @Target(value = {ElementType.TYPE, ElementType.METHOD})
085    public @interface Name {
086        /**
087         * Optional pattern to derive the test's name from the parameters. Use
088         * numbers in braces to refer to the parameters or the additional data
089         * as follows:
090         * <pre>
091         * {row} - the current row index (zero based)
092         * {0} - the row's value in the first column
093         * {1} - the row's value in the second column
094         * {ColumnA} - the row's value in the "ColumnA" column
095         * {ColumnB} - the row's value in the "ColumnB" column
096         * etc...
097         * </pre>
098         * <p>
099         * Default value is "[{row}]: {0}".
100         *
101         * @return A pattern string a bit similar to {@link MessageFormat}
102         */
103        String value() default DEFAULT_NAME;
104
105        /**
106         * Returns true if the SHORT name format shall be used instead of the default LONG name format.
107         * <p>
108         * LONG and SHORT name formats are defined as follows:
109         * <pre>
110         * false (LONG format): {@literal "<TestClass>.<TestMethod><Name>"}
111         * true (SHORT format): {@literal "<Name>"} for method level unrolling and
112         *                      {@literal "<TestMethod>"} or {@literal "<TestMethod><Name>"} for class level unrolling
113         * </pre>
114         * SHORT format is much nicer to look at but unfortunately it prevents individual test from being re-run in
115         * Intellij.
116         *
117         * @return true if SHORT format shall be used, false by default indicating LONG format
118         */
119        boolean shortFormat() default false;
120    }
121
122    /**
123     * Annotation for fields or parameters of a test method or of the test constructor. Fields
124     * need only be annotated if the field name differs from the column name of the test data.
125     * Constructor or test method parameters need to be annotated if they are not in the same
126     * order as the columns in the test data.
127     * <p>
128     * The following reference types are supported:
129     * <pre>
130     * {row} - the current row index (zero based), assignable to an integer type
131     * {*} - indicating that all rows are to be used, assignable to a collection type, a map or a Bean
132     * {ColumnA} - the value in the "ColumnA" column
133     * {ColumnB} - the value in the "ColumnB" column
134     * </pre>
135     */
136    @Retention(RetentionPolicy.RUNTIME)
137    @Target(value = {ElementType.FIELD, ElementType.PARAMETER})
138    public @interface Ref {
139        /**
140         * Returns the column name, or {row} for row index and {*} to map all column values to the annotated variable.
141         * Can be omitted when annotating fields and the field name is identical to the column name.
142         *
143         * @return the column name, or "{row}" for the row index, or "{*}" to indicate that all all column values should
144         *         be mapped to the annotated variable (for list, map and bean types)
145         */
146        String value() default "";
147    }
148
149    /**
150     * Add this annotation to your test class or method if you want to specify custom value converters from string to
151     * typed parameters. The converter must have a public zero-arg constructor.
152     */
153    @Retention(RetentionPolicy.RUNTIME)
154    @Inherited
155    @Target(value = {ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR})
156    public @interface UseValueConverter {
157        /**
158         * @return a {@link ValueConverter} class (must have a default constructor)
159         */
160        Class<? extends ValueConverter> value() default SpockitoValueConverter.class;
161    }
162
163    /**
164     * Only called reflectively. Do not use programmatically.
165     * @param clazz the test class
166     * @throws InitializationError when a problem occurs during the initialisation of the runner
167     */
168    public Spockito(final Class<?> clazz) throws InitializationError {
169        super(clazz, createRunners(clazz));
170    }
171
172    @Override
173    public void filter(final Filter filter) throws NoTestsRemainException {
174        super.filter(new MethodLevelFilter(filter));
175    }
176
177    private static Table classWideTableOrNull(final Class<?> clazz) {
178        Unroll unroll = getOnlyConstructor(clazz).getAnnotation(Unroll.class);
179        if (unroll == null) {
180            unroll = clazz.getAnnotation(Unroll.class);
181        }
182        return unroll == null ? null : Table.parse(unroll.value());
183    }
184
185    private static List<Runner> createRunners(final Class<?> clazz) throws InitializationError {
186        final ValueConverter defaultValueConverter = getDefaultValueConverter(clazz);
187        final List<Runner> runners = new ArrayList<>();
188        final Table classWideTable = classWideTableOrNull(clazz);
189        if (classWideTable != null) {
190            for (final TableRow row : classWideTable) {
191                runners.add(new SingleRowMultiTestRunner(clazz, row, defaultValueConverter));
192            }
193        } else {
194            for (final FrameworkMethod testMethod : new TestClass(clazz).getAnnotatedMethods(Test.class)) {
195                final Spockito.UseValueConverter useValueConverter = testMethod.getAnnotation(Spockito.UseValueConverter.class);
196                final ValueConverter methodValueConverter = Spockito.getValueConverter(useValueConverter, defaultValueConverter);
197                runners.add(new SingleTestMultiRowRunner(clazz, testMethod, methodValueConverter));
198            }
199        }
200        return runners;
201    }
202
203    private static Constructor<?> getOnlyConstructor(final Class<?> clazz) {
204        Constructor<?>[] constructors = clazz.getConstructors();
205        Assert.assertEquals(1, constructors.length);
206        return constructors[0];
207    }
208
209    private static ValueConverter getDefaultValueConverter(final Class<?> clazz) {
210        final Spockito.UseValueConverter useValueConverter = clazz.getAnnotation(Spockito.UseValueConverter.class);
211        return Spockito.getValueConverter(useValueConverter, SpockitoValueConverter.DEFAULT_INSTANCE);
212    }
213
214    static String parameterRefNameOrNull(final Parameter parameter) {
215        final Spockito.Ref ref = parameter.getAnnotation(Spockito.Ref.class);
216        if (ref == null) {
217            return parameter.isNamePresent() ? parameter.getName() : null;
218        } else {
219            return ref.value();
220        }
221    }
222
223    static Name nameAnnotationOrNull(final Executable executable) {
224        final Name name = executable.getAnnotation(Name.class);
225        return name != null ? name : executable.getDeclaringClass().getAnnotation(Name.class);
226    }
227    static String getName(final Executable executable, final TableRow tableRow) {
228        return getName(nameAnnotationOrNull(executable), tableRow, DEFAULT_NAME);
229    }
230    static String getName(final Name name, final TableRow tableRow, final String defaultName) {
231        final String unresolved = name != null ? name.value() : defaultName;
232        String resolved = unresolved;
233        resolved = resolved.replaceAll("\\{row\\}", String.valueOf(tableRow.getRowIndex()));
234        final Table table = tableRow.getTable();
235        for (int col = 0; col < table.getColumnCount(); col++) {
236            final String value = tableRow.get(col);
237            resolved = resolved.replaceAll("\\{" + col + "\\}", value);
238            resolved = resolved.replaceAll("\\{" + table.getColumnName(col) + "\\}", value);
239        }
240        return resolved;
241    }
242
243    static ValueConverter getValueConverter(final Spockito.UseValueConverter useValueConverter, final ValueConverter defaultValueConverter) {
244        if (useValueConverter != null) {
245            try {
246                return useValueConverter.value().newInstance();
247            } catch (final Exception e) {
248                throw new IllegalArgumentException("Could not instantiate ValueConverter of type " + useValueConverter.value(), e);
249            }
250        }
251        return defaultValueConverter;
252    }
253
254}