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.Test;
027import org.junit.runners.model.FrameworkField;
028import org.junit.runners.model.FrameworkMethod;
029import org.junit.runners.model.InitializationError;
030
031import java.lang.reflect.Constructor;
032import java.lang.reflect.Field;
033import java.util.ArrayList;
034import java.util.List;
035import java.util.Objects;
036
037/**
038 * A runner for the case of a single data row applied to a set of test methods. This case applies if the
039 * {@link org.tools4j.spockito.Spockito.Unroll} annotation is present at test class level.
040 */
041public class SingleRowMultiTestRunner extends AbstractSpockitoTestRunner {
042
043    private final TableRow tableRow;
044    private final ValueConverter defaultValueConverter;
045
046    public SingleRowMultiTestRunner(final Class<?> clazz,
047                                    final TableRow tableRow,
048                                    final ValueConverter defaultValueConverter) throws InitializationError {
049        super(clazz);
050        this.tableRow = Objects.requireNonNull(tableRow);
051        this.defaultValueConverter = Objects.requireNonNull(defaultValueConverter);
052        validate();
053    }
054
055    @Override
056    public Object createTest() throws Exception {
057        final Object testInstance = createTestUsingConstructorInjection();
058        return fieldsAreAnnotated() ? injectAnnotatedFields(testInstance) : testInstance;
059    }
060
061    private Object createTestUsingConstructorInjection() throws Exception {
062        final Constructor<?> constructor = getTestClass().getOnlyConstructor();
063        final ValueConverter valueConverter = Spockito.getValueConverter(constructor.getAnnotation(Spockito.UseValueConverter.class), defaultValueConverter);
064        final Object[] args = tableRow.convertValues(constructor, valueConverter);
065        return constructor.newInstance(args);
066    }
067
068    private Object injectAnnotatedFields(final Object testInstance) throws Exception {
069        final List<FrameworkField> fields = getFieldsAnnotatedByRef();
070        final Object[] fieldValues = tableRow.convertValues(fields, defaultValueConverter);
071        for (int i = 0; i < fields.size(); i++) {
072            final Field field = fields.get(i).getField();
073            try {
074                field.setAccessible(true);
075                field.set(testInstance, fieldValues[i]);
076            } catch (final Exception e) {
077                throw new Exception(getTestClass().getName()
078                        + ": Trying to set " + field.getName()
079                        + " with the value " + fieldValues[i], e);
080            }
081        }
082        return testInstance;
083    }
084
085    @Override
086    protected String getName() {
087        return Spockito.getName(getTestClass().getOnlyConstructor(), tableRow);
088    }
089
090    @Override
091    protected List<FrameworkMethod> computeTestMethods() {
092        final List<FrameworkMethod> testMethods = super.computeTestMethods();
093        final List<FrameworkMethod> spockitoMethods = new ArrayList<>(testMethods.size());
094        for (final FrameworkMethod testMethod : testMethods) {
095            if (testMethod.getMethod().getParameterCount() == 0) {
096                spockitoMethods.add(testMethod);
097            } else {
098                final Spockito.UseValueConverter useValueConverter = testMethod.getAnnotation(Spockito.UseValueConverter.class);
099                final ValueConverter methodValueConverter = Spockito.getValueConverter(useValueConverter, defaultValueConverter);
100                final UnrolledTestMethod spockitoTestMethod = new UnrolledTestMethod(testMethod.getMethod(), tableRow, methodValueConverter);
101                spockitoMethods.add(spockitoTestMethod);
102            }
103        }
104        return spockitoMethods;
105    }
106
107
108    @Override
109    protected String testName(final FrameworkMethod method) {
110        final String testName = super.testName(method);
111        //NOTE: we intentionally don't want class level Name annotation as a default here!
112        final Spockito.Name name = method.getAnnotation(Spockito.Name.class);
113        return name == null ? testName : testName + Spockito.getName(name, tableRow, "");
114    }
115
116    @Override
117    protected void validateConstructor(List<Throwable> errors) {
118        validateOnlyOneConstructor(errors);
119        validateConstructorArgs(errors);
120    }
121
122    @Override
123    protected void validateFields(List<Throwable> errors) {
124        super.validateFields(errors);
125        if (fieldsAreAnnotated()) {
126            final List<FrameworkField> fields = getFieldsAnnotatedByRef();
127            for (final FrameworkField field : fields) {
128                final String refName = field.getField().getAnnotation(Spockito.Ref.class).value();
129                if (!tableRow.isValidRefName(refName)) {
130                    errors.add(new Exception("Invalid @Ref value: " + refName +
131                            " does not reference a column of the table defined by @Unroll"));
132                }
133            }
134        }
135    }
136
137    protected void validateConstructorArgs(List<Throwable> errors) {
138        final Constructor<?> constructor = getTestClass().getOnlyConstructor();
139        final java.lang.reflect.Parameter[] parameters = constructor.getParameters();
140        for (int i = 0; i < parameters.length; i++) {
141            final String refName = Spockito.parameterRefNameOrNull(parameters[i]);
142            if (refName != null && !tableRow.isValidRefName(refName)) {
143                errors.add(new Exception("Invalid @Ref value or parameter name for argument " + i +
144                        " of type " + parameters[i].getType() + " in the constructor: " + refName +
145                        " does not reference a column of the table defined by @Unroll"));
146            }
147        }
148    }
149
150    protected void validate() throws InitializationError {
151        final List<Throwable> errors = new ArrayList<>();
152        super.collectInitializationErrors(errors);
153    }
154
155    @Override
156    protected void validateTestMethods(final List<Throwable> errors) {
157        final List<FrameworkMethod> methods = getTestClass().getAnnotatedMethods(Test.class);
158        for (final FrameworkMethod method : methods) {
159            method.validatePublicVoid(false, errors);
160            method.validateNoTypeParametersOnArgs(errors);
161        }
162    }
163
164    private List<FrameworkField> getFieldsAnnotatedByRef() {
165        return getTestClass().getAnnotatedFields(Spockito.Ref.class);
166    }
167
168    private boolean fieldsAreAnnotated() {
169        return !getFieldsAnnotatedByRef().isEmpty();
170    }
171
172}