diff options
Diffstat (limited to 'src/main/java/org/junit/experimental/theories')
17 files changed, 1231 insertions, 570 deletions
diff --git a/src/main/java/org/junit/experimental/theories/DataPoint.java b/src/main/java/org/junit/experimental/theories/DataPoint.java index 2aaba6a..0a017bb 100644 --- a/src/main/java/org/junit/experimental/theories/DataPoint.java +++ b/src/main/java/org/junit/experimental/theories/DataPoint.java @@ -1,9 +1,56 @@ package org.junit.experimental.theories; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +/** + * Annotating an field or method with @DataPoint will cause the field value + * or the value returned by the method to be used as a potential parameter for + * theories in that class, when run with the + * {@link org.junit.experimental.theories.Theories Theories} runner. + * <p> + * A DataPoint is only considered as a potential value for parameters for + * which its type is assignable. When multiple {@code DataPoint}s exist + * with overlapping types more control can be obtained by naming each DataPoint + * using the value of this annotation, e.g. with + * <code>@DataPoint({"dataset1", "dataset2"})</code>, and then specifying + * which named set to consider as potential values for each parameter using the + * {@link org.junit.experimental.theories.FromDataPoints @FromDataPoints} + * annotation. + * <p> + * Parameters with no specified source (i.e. without @FromDataPoints or + * other {@link org.junit.experimental.theories.ParametersSuppliedBy + * @ParameterSuppliedBy} annotations) will use all {@code DataPoint}s that are + * assignable to the parameter type as potential values, including named sets of + * {@code DataPoint}s. + * + * <pre> + * @DataPoint + * public static String dataPoint = "value"; + * + * @DataPoint("generated") + * public static String generatedDataPoint() { + * return "generated value"; + * } + * + * @Theory + * public void theoryMethod(String param) { + * ... + * } + * </pre> + * + * @see org.junit.experimental.theories.Theories + * @see org.junit.experimental.theories.Theory + * @see org.junit.experimental.theories.DataPoint + * @see org.junit.experimental.theories.FromDataPoints + */ @Retention(RetentionPolicy.RUNTIME) +@Target({FIELD, METHOD}) public @interface DataPoint { - -} + String[] value() default {}; + Class<? extends Throwable>[] ignoredExceptions() default {}; +}
\ No newline at end of file diff --git a/src/main/java/org/junit/experimental/theories/DataPoints.java b/src/main/java/org/junit/experimental/theories/DataPoints.java index 42145e3..b47461b 100644 --- a/src/main/java/org/junit/experimental/theories/DataPoints.java +++ b/src/main/java/org/junit/experimental/theories/DataPoints.java @@ -1,9 +1,64 @@ package org.junit.experimental.theories; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +/** + * Annotating an array or iterable-typed field or method with @DataPoints + * will cause the values in the array or iterable given to be used as potential + * parameters for theories in that class when run with the + * {@link org.junit.experimental.theories.Theories Theories} runner. + * <p> + * DataPoints will only be considered as potential values for parameters for + * which their types are assignable. When multiple sets of DataPoints exist with + * overlapping types more control can be obtained by naming the DataPoints using + * the value of this annotation, e.g. with + * <code>@DataPoints({"dataset1", "dataset2"})</code>, and then specifying + * which named set to consider as potential values for each parameter using the + * {@link org.junit.experimental.theories.FromDataPoints @FromDataPoints} + * annotation. + * <p> + * Parameters with no specified source (i.e. without @FromDataPoints or + * other {@link org.junit.experimental.theories.ParametersSuppliedBy + * @ParameterSuppliedBy} annotations) will use all DataPoints that are + * assignable to the parameter type as potential values, including named sets of + * DataPoints. + * <p> + * DataPoints methods whose array types aren't assignable from the target + * parameter type (and so can't possibly return relevant values) will not be + * called when generating values for that parameter. Iterable-typed datapoints + * methods must always be called though, as this information is not available + * here after generic type erasure, so expensive methods returning iterable + * datapoints are a bad idea. + * + * <pre> + * @DataPoints + * public static String[] dataPoints = new String[] { ... }; + * + * @DataPoints + * public static String[] generatedDataPoints() { + * return new String[] { ... }; + * } + * + * @Theory + * public void theoryMethod(String param) { + * ... + * } + * </pre> + * + * @see org.junit.experimental.theories.Theories + * @see org.junit.experimental.theories.Theory + * @see org.junit.experimental.theories.DataPoint + * @see org.junit.experimental.theories.FromDataPoints + */ @Retention(RetentionPolicy.RUNTIME) +@Target({ FIELD, METHOD }) public @interface DataPoints { + String[] value() default {}; + Class<? extends Throwable>[] ignoredExceptions() default {}; } diff --git a/src/main/java/org/junit/experimental/theories/FromDataPoints.java b/src/main/java/org/junit/experimental/theories/FromDataPoints.java new file mode 100644 index 0000000..2b149ca --- /dev/null +++ b/src/main/java/org/junit/experimental/theories/FromDataPoints.java @@ -0,0 +1,54 @@ +package org.junit.experimental.theories; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.experimental.theories.internal.SpecificDataPointsSupplier; + +/** + * Annotating a parameter of a {@link org.junit.experimental.theories.Theory + * @Theory} method with <code>@FromDataPoints</code> will limit the + * datapoints considered as potential values for that parameter to just the + * {@link org.junit.experimental.theories.DataPoints DataPoints} with the given + * name. DataPoint names can be given as the value parameter of the + * @DataPoints annotation. + * <p> + * DataPoints without names will not be considered as values for any parameters + * annotated with @FromDataPoints. + * <pre> + * @DataPoints + * public static String[] unnamed = new String[] { ... }; + * + * @DataPoints("regexes") + * public static String[] regexStrings = new String[] { ... }; + * + * @DataPoints({"forMatching", "alphanumeric"}) + * public static String[] testStrings = new String[] { ... }; + * + * @Theory + * public void stringTheory(String param) { + * // This will be called with every value in 'regexStrings', + * // 'testStrings' and 'unnamed'. + * } + * + * @Theory + * public void regexTheory(@FromDataPoints("regexes") String regex, + * @FromDataPoints("forMatching") String value) { + * // This will be called with only the values in 'regexStrings' as + * // regex, only the values in 'testStrings' as value, and none + * // of the values in 'unnamed'. + * } + * </pre> + * + * @see org.junit.experimental.theories.Theory + * @see org.junit.experimental.theories.DataPoint + * @see org.junit.experimental.theories.DataPoints + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +@ParametersSuppliedBy(SpecificDataPointsSupplier.class) +public @interface FromDataPoints { + String value(); +} diff --git a/src/main/java/org/junit/experimental/theories/ParameterSignature.java b/src/main/java/org/junit/experimental/theories/ParameterSignature.java index e7150fc..cf22583 100644 --- a/src/main/java/org/junit/experimental/theories/ParameterSignature.java +++ b/src/main/java/org/junit/experimental/theories/ParameterSignature.java @@ -1,6 +1,3 @@ -/** - * - */ package org.junit.experimental.theories; import java.lang.annotation.Annotation; @@ -8,83 +5,130 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; public class ParameterSignature { - public static ArrayList<ParameterSignature> signatures(Method method) { - return signatures(method.getParameterTypes(), method - .getParameterAnnotations()); - } - - public static List<ParameterSignature> signatures(Constructor<?> constructor) { - return signatures(constructor.getParameterTypes(), constructor - .getParameterAnnotations()); - } - - private static ArrayList<ParameterSignature> signatures( - Class<?>[] parameterTypes, Annotation[][] parameterAnnotations) { - ArrayList<ParameterSignature> sigs= new ArrayList<ParameterSignature>(); - for (int i= 0; i < parameterTypes.length; i++) { - sigs.add(new ParameterSignature(parameterTypes[i], - parameterAnnotations[i])); - } - return sigs; - } - - private final Class<?> type; - - private final Annotation[] annotations; - - private ParameterSignature(Class<?> type, Annotation[] annotations) { - this.type= type; - this.annotations= annotations; - } - - public boolean canAcceptType(Class<?> candidate) { - return type.isAssignableFrom(candidate); - } - - public Class<?> getType() { - return type; - } - - public List<Annotation> getAnnotations() { - return Arrays.asList(annotations); - } - - public boolean canAcceptArrayType(Class<?> type) { - return type.isArray() && canAcceptType(type.getComponentType()); - } - - public boolean hasAnnotation(Class<? extends Annotation> type) { - return getAnnotation(type) != null; - } - - public <T extends Annotation> T findDeepAnnotation(Class<T> annotationType) { - Annotation[] annotations2= annotations; - return findDeepAnnotation(annotations2, annotationType, 3); - } - - private <T extends Annotation> T findDeepAnnotation( - Annotation[] annotations, Class<T> annotationType, int depth) { - if (depth == 0) - return null; - for (Annotation each : annotations) { - if (annotationType.isInstance(each)) - return annotationType.cast(each); - Annotation candidate= findDeepAnnotation(each.annotationType() - .getAnnotations(), annotationType, depth - 1); - if (candidate != null) - return annotationType.cast(candidate); - } - - return null; - } - - public <T extends Annotation> T getAnnotation(Class<T> annotationType) { - for (Annotation each : getAnnotations()) - if (annotationType.isInstance(each)) - return annotationType.cast(each); - return null; - } + + private static final Map<Class<?>, Class<?>> CONVERTABLE_TYPES_MAP = buildConvertableTypesMap(); + + private static Map<Class<?>, Class<?>> buildConvertableTypesMap() { + Map<Class<?>, Class<?>> map = new HashMap<Class<?>, Class<?>>(); + + putSymmetrically(map, boolean.class, Boolean.class); + putSymmetrically(map, byte.class, Byte.class); + putSymmetrically(map, short.class, Short.class); + putSymmetrically(map, char.class, Character.class); + putSymmetrically(map, int.class, Integer.class); + putSymmetrically(map, long.class, Long.class); + putSymmetrically(map, float.class, Float.class); + putSymmetrically(map, double.class, Double.class); + + return Collections.unmodifiableMap(map); + } + + private static <T> void putSymmetrically(Map<T, T> map, T a, T b) { + map.put(a, b); + map.put(b, a); + } + + public static ArrayList<ParameterSignature> signatures(Method method) { + return signatures(method.getParameterTypes(), method + .getParameterAnnotations()); + } + + public static List<ParameterSignature> signatures(Constructor<?> constructor) { + return signatures(constructor.getParameterTypes(), constructor + .getParameterAnnotations()); + } + + private static ArrayList<ParameterSignature> signatures( + Class<?>[] parameterTypes, Annotation[][] parameterAnnotations) { + ArrayList<ParameterSignature> sigs = new ArrayList<ParameterSignature>(); + for (int i = 0; i < parameterTypes.length; i++) { + sigs.add(new ParameterSignature(parameterTypes[i], + parameterAnnotations[i])); + } + return sigs; + } + + private final Class<?> type; + + private final Annotation[] annotations; + + private ParameterSignature(Class<?> type, Annotation[] annotations) { + this.type = type; + this.annotations = annotations; + } + + public boolean canAcceptValue(Object candidate) { + return (candidate == null) ? !type.isPrimitive() : canAcceptType(candidate.getClass()); + } + + public boolean canAcceptType(Class<?> candidate) { + return type.isAssignableFrom(candidate) || + isAssignableViaTypeConversion(type, candidate); + } + + public boolean canPotentiallyAcceptType(Class<?> candidate) { + return candidate.isAssignableFrom(type) || + isAssignableViaTypeConversion(candidate, type) || + canAcceptType(candidate); + } + + private boolean isAssignableViaTypeConversion(Class<?> targetType, Class<?> candidate) { + if (CONVERTABLE_TYPES_MAP.containsKey(candidate)) { + Class<?> wrapperClass = CONVERTABLE_TYPES_MAP.get(candidate); + return targetType.isAssignableFrom(wrapperClass); + } else { + return false; + } + } + + public Class<?> getType() { + return type; + } + + public List<Annotation> getAnnotations() { + return Arrays.asList(annotations); + } + + public boolean hasAnnotation(Class<? extends Annotation> type) { + return getAnnotation(type) != null; + } + + public <T extends Annotation> T findDeepAnnotation(Class<T> annotationType) { + Annotation[] annotations2 = annotations; + return findDeepAnnotation(annotations2, annotationType, 3); + } + + private <T extends Annotation> T findDeepAnnotation( + Annotation[] annotations, Class<T> annotationType, int depth) { + if (depth == 0) { + return null; + } + for (Annotation each : annotations) { + if (annotationType.isInstance(each)) { + return annotationType.cast(each); + } + Annotation candidate = findDeepAnnotation(each.annotationType() + .getAnnotations(), annotationType, depth - 1); + if (candidate != null) { + return annotationType.cast(candidate); + } + } + + return null; + } + + public <T extends Annotation> T getAnnotation(Class<T> annotationType) { + for (Annotation each : getAnnotations()) { + if (annotationType.isInstance(each)) { + return annotationType.cast(each); + } + } + return null; + } }
\ No newline at end of file diff --git a/src/main/java/org/junit/experimental/theories/ParameterSupplier.java b/src/main/java/org/junit/experimental/theories/ParameterSupplier.java index 9016c91..bac8b34 100644 --- a/src/main/java/org/junit/experimental/theories/ParameterSupplier.java +++ b/src/main/java/org/junit/experimental/theories/ParameterSupplier.java @@ -2,7 +2,42 @@ package org.junit.experimental.theories; import java.util.List; - +/** + * Abstract parent class for suppliers of input data points for theories. Extend this class to customize how {@link + * org.junit.experimental.theories.Theories Theories} runner + * finds accepted data points. Then use your class together with <b>@ParametersSuppliedBy</b> on input + * parameters for theories. + * + * <p> + * For example, here is a supplier for values between two integers, and an annotation that references it: + * + * <pre> + * @Retention(RetentionPolicy.RUNTIME) + * <b>@ParametersSuppliedBy</b>(BetweenSupplier.class) + * public @interface Between { + * int first(); + * + * int last(); + * } + * + * public static class BetweenSupplier extends <b>ParameterSupplier</b> { + * @Override + * public List<<b>PotentialAssignment</b>> getValueSources(<b>ParameterSignature</b> sig) { + * List<<b>PotentialAssignment</b>> list = new ArrayList<PotentialAssignment>(); + * Between annotation = (Between) sig.getSupplierAnnotation(); + * + * for (int i = annotation.first(); i <= annotation.last(); i++) + * list.add(<b>PotentialAssignment</b>.forValue("ints", i)); + * return list; + * } + * } + * </pre> + * </p> + * + * @see org.junit.experimental.theories.ParametersSuppliedBy + * @see org.junit.experimental.theories.Theories + * @see org.junit.experimental.theories.FromDataPoints + */ public abstract class ParameterSupplier { - public abstract List<PotentialAssignment> getValueSources(ParameterSignature sig); + public abstract List<PotentialAssignment> getValueSources(ParameterSignature sig) throws Throwable; } diff --git a/src/main/java/org/junit/experimental/theories/ParametersSuppliedBy.java b/src/main/java/org/junit/experimental/theories/ParametersSuppliedBy.java index 8f090ef..15b5d95 100644 --- a/src/main/java/org/junit/experimental/theories/ParametersSuppliedBy.java +++ b/src/main/java/org/junit/experimental/theories/ParametersSuppliedBy.java @@ -1,12 +1,48 @@ package org.junit.experimental.theories; +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.PARAMETER; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; - +/** + * Annotating a {@link org.junit.experimental.theories.Theory Theory} method + * parameter with @ParametersSuppliedBy causes it to be supplied with + * values from the named + * {@link org.junit.experimental.theories.ParameterSupplier ParameterSupplier} + * when run as a theory by the {@link org.junit.experimental.theories.Theories + * Theories} runner. + * + * In addition, annotations themselves can be annotated with + * @ParametersSuppliedBy, and then used similarly. ParameterSuppliedBy + * annotations on parameters are detected by searching up this heirarchy such + * that these act as syntactic sugar, making: + * + * <pre> + * @ParametersSuppliedBy(Supplier.class) + * public @interface SpecialParameter { } + * + * @Theory + * public void theoryMethod(@SpecialParameter String param) { + * ... + * } + * </pre> + * + * equivalent to: + * + * <pre> + * @Theory + * public void theoryMethod(@ParametersSuppliedBy(Supplier.class) String param) { + * ... + * } + * </pre> + */ @Retention(RetentionPolicy.RUNTIME) +@Target({ ANNOTATION_TYPE, PARAMETER }) public @interface ParametersSuppliedBy { - Class<? extends ParameterSupplier> value(); + Class<? extends ParameterSupplier> value(); } diff --git a/src/main/java/org/junit/experimental/theories/PotentialAssignment.java b/src/main/java/org/junit/experimental/theories/PotentialAssignment.java index 0c008d0..18ca07a 100644 --- a/src/main/java/org/junit/experimental/theories/PotentialAssignment.java +++ b/src/main/java/org/junit/experimental/theories/PotentialAssignment.java @@ -1,31 +1,52 @@ package org.junit.experimental.theories; +import static java.lang.String.format; + public abstract class PotentialAssignment { - public static class CouldNotGenerateValueException extends Exception { - private static final long serialVersionUID= 1L; - } - - public static PotentialAssignment forValue(final String name, final Object value) { - return new PotentialAssignment() { - @Override - public Object getValue() throws CouldNotGenerateValueException { - return value; - } - - @Override - public String toString() { - return String.format("[%s]", value); - } - - @Override - public String getDescription() - throws CouldNotGenerateValueException { - return name; - } - }; - } - - public abstract Object getValue() throws CouldNotGenerateValueException; - - public abstract String getDescription() throws CouldNotGenerateValueException; -} + public static class CouldNotGenerateValueException extends Exception { + private static final long serialVersionUID = 1L; + + public CouldNotGenerateValueException() { + } + + public CouldNotGenerateValueException(Throwable e) { + super(e); + } + } + + public static PotentialAssignment forValue(final String name, final Object value) { + return new PotentialAssignment() { + @Override + public Object getValue() { + return value; + } + + @Override + public String toString() { + return format("[%s]", value); + } + + @Override + public String getDescription() { + String valueString; + + if (value == null) { + valueString = "null"; + } else { + try { + valueString = format("\"%s\"", value); + } catch (Throwable e) { + valueString = format("[toString() threw %s: %s]", + e.getClass().getSimpleName(), e.getMessage()); + } + } + + return format("%s <from %s>", valueString, name); + } + }; + } + + public abstract Object getValue() throws CouldNotGenerateValueException; + + public abstract String getDescription() throws CouldNotGenerateValueException; +}
\ No newline at end of file diff --git a/src/main/java/org/junit/experimental/theories/Theories.java b/src/main/java/org/junit/experimental/theories/Theories.java index 82ff98b..817f553 100644 --- a/src/main/java/org/junit/experimental/theories/Theories.java +++ b/src/main/java/org/junit/experimental/theories/Theories.java @@ -1,16 +1,14 @@ -/** - * - */ package org.junit.experimental.theories; +import java.lang.reflect.Constructor; import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.List; import org.junit.Assert; -import org.junit.experimental.theories.PotentialAssignment.CouldNotGenerateValueException; +import org.junit.Assume; import org.junit.experimental.theories.internal.Assignments; import org.junit.experimental.theories.internal.ParameterizedAssertionError; import org.junit.internal.AssumptionViolatedException; @@ -20,180 +18,288 @@ import org.junit.runners.model.InitializationError; import org.junit.runners.model.Statement; import org.junit.runners.model.TestClass; +/** + * The Theories runner allows to test a certain functionality against a subset of an infinite set of data points. + * <p> + * A Theory is a piece of functionality (a method) that is executed against several data inputs called data points. + * To make a test method a theory you mark it with <b>@Theory</b>. To create a data point you create a public + * field in your test class and mark it with <b>@DataPoint</b>. The Theories runner then executes your test + * method as many times as the number of data points declared, providing a different data point as + * the input argument on each invocation. + * </p> + * <p> + * A Theory differs from standard test method in that it captures some aspect of the intended behavior in possibly + * infinite numbers of scenarios which corresponds to the number of data points declared. Using assumptions and + * assertions properly together with covering multiple scenarios with different data points can make your tests more + * flexible and bring them closer to scientific theories (hence the name). + * </p> + * <p> + * For example: + * <pre> + * + * @RunWith(<b>Theories.class</b>) + * public class UserTest { + * <b>@DataPoint</b> + * public static String GOOD_USERNAME = "optimus"; + * <b>@DataPoint</b> + * public static String USERNAME_WITH_SLASH = "optimus/prime"; + * + * <b>@Theory</b> + * public void filenameIncludesUsername(String username) { + * assumeThat(username, not(containsString("/"))); + * assertThat(new User(username).configFileName(), containsString(username)); + * } + * } + * </pre> + * This makes it clear that the user's filename should be included in the config file name, + * only if it doesn't contain a slash. Another test or theory might define what happens when a username does contain + * a slash. <code>UserTest</code> will attempt to run <code>filenameIncludesUsername</code> on every compatible data + * point defined in the class. If any of the assumptions fail, the data point is silently ignored. If all of the + * assumptions pass, but an assertion fails, the test fails. + * <p> + * Defining general statements as theories allows data point reuse across a bunch of functionality tests and also + * allows automated tools to search for new, unexpected data points that expose bugs. + * </p> + * <p> + * The support for Theories has been absorbed from the Popper project, and more complete documentation can be found + * from that projects archived documentation. + * </p> + * + * @see <a href="http://web.archive.org/web/20071012143326/popper.tigris.org/tutorial.html">Archived Popper project documentation</a> + * @see <a href="http://web.archive.org/web/20110608210825/http://shareandenjoy.saff.net/tdd-specifications.pdf">Paper on Theories</a> + */ public class Theories extends BlockJUnit4ClassRunner { - public Theories(Class<?> klass) throws InitializationError { - super(klass); - } - - @Override - protected void collectInitializationErrors(List<Throwable> errors) { - super.collectInitializationErrors(errors); - validateDataPointFields(errors); - } - - private void validateDataPointFields(List<Throwable> errors) { - Field[] fields= getTestClass().getJavaClass().getDeclaredFields(); - - for (Field each : fields) - if (each.getAnnotation(DataPoint.class) != null && !Modifier.isStatic(each.getModifiers())) - errors.add(new Error("DataPoint field " + each.getName() + " must be static")); - } - - @Override - protected void validateConstructor(List<Throwable> errors) { - validateOnlyOneConstructor(errors); - } - - @Override - protected void validateTestMethods(List<Throwable> errors) { - for (FrameworkMethod each : computeTestMethods()) - if(each.getAnnotation(Theory.class) != null) - each.validatePublicVoid(false, errors); - else - each.validatePublicVoidNoArg(false, errors); - } - - @Override - protected List<FrameworkMethod> computeTestMethods() { - List<FrameworkMethod> testMethods= super.computeTestMethods(); - List<FrameworkMethod> theoryMethods= getTestClass().getAnnotatedMethods(Theory.class); - testMethods.removeAll(theoryMethods); - testMethods.addAll(theoryMethods); - return testMethods; - } - - @Override - public Statement methodBlock(final FrameworkMethod method) { - return new TheoryAnchor(method, getTestClass()); - } - - public static class TheoryAnchor extends Statement { - private int successes= 0; - - private FrameworkMethod fTestMethod; - private TestClass fTestClass; - - private List<AssumptionViolatedException> fInvalidParameters= new ArrayList<AssumptionViolatedException>(); - - public TheoryAnchor(FrameworkMethod method, TestClass testClass) { - fTestMethod= method; - fTestClass= testClass; - } + public Theories(Class<?> klass) throws InitializationError { + super(klass); + } + + @Override + protected void collectInitializationErrors(List<Throwable> errors) { + super.collectInitializationErrors(errors); + validateDataPointFields(errors); + validateDataPointMethods(errors); + } + + private void validateDataPointFields(List<Throwable> errors) { + Field[] fields = getTestClass().getJavaClass().getDeclaredFields(); + + for (Field field : fields) { + if (field.getAnnotation(DataPoint.class) == null && field.getAnnotation(DataPoints.class) == null) { + continue; + } + if (!Modifier.isStatic(field.getModifiers())) { + errors.add(new Error("DataPoint field " + field.getName() + " must be static")); + } + if (!Modifier.isPublic(field.getModifiers())) { + errors.add(new Error("DataPoint field " + field.getName() + " must be public")); + } + } + } + + private void validateDataPointMethods(List<Throwable> errors) { + Method[] methods = getTestClass().getJavaClass().getDeclaredMethods(); + + for (Method method : methods) { + if (method.getAnnotation(DataPoint.class) == null && method.getAnnotation(DataPoints.class) == null) { + continue; + } + if (!Modifier.isStatic(method.getModifiers())) { + errors.add(new Error("DataPoint method " + method.getName() + " must be static")); + } + if (!Modifier.isPublic(method.getModifiers())) { + errors.add(new Error("DataPoint method " + method.getName() + " must be public")); + } + } + } + + @Override + protected void validateConstructor(List<Throwable> errors) { + validateOnlyOneConstructor(errors); + } + + @Override + protected void validateTestMethods(List<Throwable> errors) { + for (FrameworkMethod each : computeTestMethods()) { + if (each.getAnnotation(Theory.class) != null) { + each.validatePublicVoid(false, errors); + each.validateNoTypeParametersOnArgs(errors); + } else { + each.validatePublicVoidNoArg(false, errors); + } + + for (ParameterSignature signature : ParameterSignature.signatures(each.getMethod())) { + ParametersSuppliedBy annotation = signature.findDeepAnnotation(ParametersSuppliedBy.class); + if (annotation != null) { + validateParameterSupplier(annotation.value(), errors); + } + } + } + } + + private void validateParameterSupplier(Class<? extends ParameterSupplier> supplierClass, List<Throwable> errors) { + Constructor<?>[] constructors = supplierClass.getConstructors(); + + if (constructors.length != 1) { + errors.add(new Error("ParameterSupplier " + supplierClass.getName() + + " must have only one constructor (either empty or taking only a TestClass)")); + } else { + Class<?>[] paramTypes = constructors[0].getParameterTypes(); + if (!(paramTypes.length == 0) && !paramTypes[0].equals(TestClass.class)) { + errors.add(new Error("ParameterSupplier " + supplierClass.getName() + + " constructor must take either nothing or a single TestClass instance")); + } + } + } + + @Override + protected List<FrameworkMethod> computeTestMethods() { + List<FrameworkMethod> testMethods = new ArrayList<FrameworkMethod>(super.computeTestMethods()); + List<FrameworkMethod> theoryMethods = getTestClass().getAnnotatedMethods(Theory.class); + testMethods.removeAll(theoryMethods); + testMethods.addAll(theoryMethods); + return testMethods; + } + + @Override + public Statement methodBlock(final FrameworkMethod method) { + return new TheoryAnchor(method, getTestClass()); + } + + public static class TheoryAnchor extends Statement { + private int successes = 0; + + private final FrameworkMethod testMethod; + private final TestClass testClass; + + private List<AssumptionViolatedException> fInvalidParameters = new ArrayList<AssumptionViolatedException>(); + + public TheoryAnchor(FrameworkMethod testMethod, TestClass testClass) { + this.testMethod = testMethod; + this.testClass = testClass; + } private TestClass getTestClass() { - return fTestClass; + return testClass; } - @Override - public void evaluate() throws Throwable { - runWithAssignment(Assignments.allUnassigned( - fTestMethod.getMethod(), getTestClass())); - - if (successes == 0) - Assert - .fail("Never found parameters that satisfied method assumptions. Violated assumptions: " - + fInvalidParameters); - } - - protected void runWithAssignment(Assignments parameterAssignment) - throws Throwable { - if (!parameterAssignment.isComplete()) { - runWithIncompleteAssignment(parameterAssignment); - } else { - runWithCompleteAssignment(parameterAssignment); - } - } - - protected void runWithIncompleteAssignment(Assignments incomplete) - throws InstantiationException, IllegalAccessException, - Throwable { - for (PotentialAssignment source : incomplete - .potentialsForNextUnassigned()) { - runWithAssignment(incomplete.assignNext(source)); - } - } - - protected void runWithCompleteAssignment(final Assignments complete) - throws InstantiationException, IllegalAccessException, - InvocationTargetException, NoSuchMethodException, Throwable { - new BlockJUnit4ClassRunner(getTestClass().getJavaClass()) { - @Override - protected void collectInitializationErrors( - List<Throwable> errors) { - // do nothing - } - - @Override - public Statement methodBlock(FrameworkMethod method) { - final Statement statement= super.methodBlock(method); - return new Statement() { - @Override - public void evaluate() throws Throwable { - try { - statement.evaluate(); - handleDataPointSuccess(); - } catch (AssumptionViolatedException e) { - handleAssumptionViolation(e); - } catch (Throwable e) { - reportParameterizedError(e, complete - .getArgumentStrings(nullsOk())); - } - } - - }; - } - - @Override - protected Statement methodInvoker(FrameworkMethod method, Object test) { - return methodCompletesWithParameters(method, complete, test); - } - - @Override - public Object createTest() throws Exception { - return getTestClass().getOnlyConstructor().newInstance( - complete.getConstructorArguments(nullsOk())); - } - }.methodBlock(fTestMethod).evaluate(); - } - - private Statement methodCompletesWithParameters( - final FrameworkMethod method, final Assignments complete, final Object freshInstance) { - return new Statement() { - @Override - public void evaluate() throws Throwable { - try { - final Object[] values= complete.getMethodArguments( - nullsOk()); - method.invokeExplosively(freshInstance, values); - } catch (CouldNotGenerateValueException e) { - // ignore - } - } - }; - } - - protected void handleAssumptionViolation(AssumptionViolatedException e) { - fInvalidParameters.add(e); - } - - protected void reportParameterizedError(Throwable e, Object... params) - throws Throwable { - if (params.length == 0) - throw e; - throw new ParameterizedAssertionError(e, fTestMethod.getName(), - params); - } - - private boolean nullsOk() { - Theory annotation= fTestMethod.getMethod().getAnnotation( - Theory.class); - if (annotation == null) - return false; - return annotation.nullsAccepted(); - } - - protected void handleDataPointSuccess() { - successes++; - } - } + @Override + public void evaluate() throws Throwable { + runWithAssignment(Assignments.allUnassigned( + testMethod.getMethod(), getTestClass())); + + //if this test method is not annotated with Theory, then no successes is a valid case + boolean hasTheoryAnnotation = testMethod.getAnnotation(Theory.class) != null; + if (successes == 0 && hasTheoryAnnotation) { + Assert + .fail("Never found parameters that satisfied method assumptions. Violated assumptions: " + + fInvalidParameters); + } + } + + protected void runWithAssignment(Assignments parameterAssignment) + throws Throwable { + if (!parameterAssignment.isComplete()) { + runWithIncompleteAssignment(parameterAssignment); + } else { + runWithCompleteAssignment(parameterAssignment); + } + } + + protected void runWithIncompleteAssignment(Assignments incomplete) + throws Throwable { + for (PotentialAssignment source : incomplete + .potentialsForNextUnassigned()) { + runWithAssignment(incomplete.assignNext(source)); + } + } + + protected void runWithCompleteAssignment(final Assignments complete) + throws Throwable { + new BlockJUnit4ClassRunner(getTestClass().getJavaClass()) { + @Override + protected void collectInitializationErrors( + List<Throwable> errors) { + // do nothing + } + + @Override + public Statement methodBlock(FrameworkMethod method) { + final Statement statement = super.methodBlock(method); + return new Statement() { + @Override + public void evaluate() throws Throwable { + try { + statement.evaluate(); + handleDataPointSuccess(); + } catch (AssumptionViolatedException e) { + handleAssumptionViolation(e); + } catch (Throwable e) { + reportParameterizedError(e, complete + .getArgumentStrings(nullsOk())); + } + } + + }; + } + + @Override + protected Statement methodInvoker(FrameworkMethod method, Object test) { + return methodCompletesWithParameters(method, complete, test); + } + + @Override + public Object createTest() throws Exception { + Object[] params = complete.getConstructorArguments(); + + if (!nullsOk()) { + Assume.assumeNotNull(params); + } + + return getTestClass().getOnlyConstructor().newInstance(params); + } + }.methodBlock(testMethod).evaluate(); + } + + private Statement methodCompletesWithParameters( + final FrameworkMethod method, final Assignments complete, final Object freshInstance) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + final Object[] values = complete.getMethodArguments(); + + if (!nullsOk()) { + Assume.assumeNotNull(values); + } + + method.invokeExplosively(freshInstance, values); + } + }; + } + + protected void handleAssumptionViolation(AssumptionViolatedException e) { + fInvalidParameters.add(e); + } + + protected void reportParameterizedError(Throwable e, Object... params) + throws Throwable { + if (params.length == 0) { + throw e; + } + throw new ParameterizedAssertionError(e, testMethod.getName(), + params); + } + + private boolean nullsOk() { + Theory annotation = testMethod.getMethod().getAnnotation( + Theory.class); + if (annotation == null) { + return false; + } + return annotation.nullsAccepted(); + } + + protected void handleDataPointSuccess() { + successes++; + } + } } diff --git a/src/main/java/org/junit/experimental/theories/Theory.java b/src/main/java/org/junit/experimental/theories/Theory.java index 134fe9d..0b9f2c4 100644 --- a/src/main/java/org/junit/experimental/theories/Theory.java +++ b/src/main/java/org/junit/experimental/theories/Theory.java @@ -1,12 +1,18 @@ -/** - * - */ package org.junit.experimental.theories; +import static java.lang.annotation.ElementType.METHOD; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +/** + * Marks test methods that should be read as theories by the {@link org.junit.experimental.theories.Theories Theories} runner. + * + * @see org.junit.experimental.theories.Theories + */ @Retention(RetentionPolicy.RUNTIME) +@Target(METHOD) public @interface Theory { - boolean nullsAccepted() default true; + boolean nullsAccepted() default true; }
\ No newline at end of file diff --git a/src/main/java/org/junit/experimental/theories/internal/AllMembersSupplier.java b/src/main/java/org/junit/experimental/theories/internal/AllMembersSupplier.java index 615cc3e..f15fb28 100644 --- a/src/main/java/org/junit/experimental/theories/internal/AllMembersSupplier.java +++ b/src/main/java/org/junit/experimental/theories/internal/AllMembersSupplier.java @@ -1,19 +1,19 @@ -/** - * - */ package org.junit.experimental.theories.internal; import java.lang.reflect.Array; import java.lang.reflect.Field; -import java.lang.reflect.Modifier; import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; import java.util.List; +import org.junit.Assume; import org.junit.experimental.theories.DataPoint; import org.junit.experimental.theories.DataPoints; import org.junit.experimental.theories.ParameterSignature; import org.junit.experimental.theories.ParameterSupplier; import org.junit.experimental.theories.PotentialAssignment; +import org.junit.runners.model.FrameworkField; import org.junit.runners.model.FrameworkMethod; import org.junit.runners.model.TestClass; @@ -21,107 +21,184 @@ import org.junit.runners.model.TestClass; * Supplies Theory parameters based on all public members of the target class. */ public class AllMembersSupplier extends ParameterSupplier { - static class MethodParameterValue extends PotentialAssignment { - private final FrameworkMethod fMethod; - - private MethodParameterValue(FrameworkMethod dataPointMethod) { - fMethod= dataPointMethod; - } - - @Override - public Object getValue() throws CouldNotGenerateValueException { - try { - return fMethod.invokeExplosively(null); - } catch (IllegalArgumentException e) { - throw new RuntimeException( - "unexpected: argument length is checked"); - } catch (IllegalAccessException e) { - throw new RuntimeException( - "unexpected: getMethods returned an inaccessible method"); - } catch (Throwable e) { - throw new CouldNotGenerateValueException(); - // do nothing, just look for more values - } - } - - @Override - public String getDescription() throws CouldNotGenerateValueException { - return fMethod.getName(); - } - } - - private final TestClass fClass; - - /** - * Constructs a new supplier for {@code type} - */ - public AllMembersSupplier(TestClass type) { - fClass= type; - } - - @Override - public List<PotentialAssignment> getValueSources(ParameterSignature sig) { - List<PotentialAssignment> list= new ArrayList<PotentialAssignment>(); - - addFields(sig, list); - addSinglePointMethods(sig, list); - addMultiPointMethods(list); - - return list; - } - - private void addMultiPointMethods(List<PotentialAssignment> list) { - for (FrameworkMethod dataPointsMethod : fClass - .getAnnotatedMethods(DataPoints.class)) - try { - addArrayValues(dataPointsMethod.getName(), list, dataPointsMethod.invokeExplosively(null)); - } catch (Throwable e) { - // ignore and move on - } - } - - @SuppressWarnings("deprecation") - private void addSinglePointMethods(ParameterSignature sig, - List<PotentialAssignment> list) { - for (FrameworkMethod dataPointMethod : fClass - .getAnnotatedMethods(DataPoint.class)) { - Class<?> type= sig.getType(); - if ((dataPointMethod.producesType(type))) - list.add(new MethodParameterValue(dataPointMethod)); - } - } - - private void addFields(ParameterSignature sig, - List<PotentialAssignment> list) { - for (final Field field : fClass.getJavaClass().getFields()) { - if (Modifier.isStatic(field.getModifiers())) { - Class<?> type= field.getType(); - if (sig.canAcceptArrayType(type) - && field.getAnnotation(DataPoints.class) != null) { - addArrayValues(field.getName(), list, getStaticFieldValue(field)); - } else if (sig.canAcceptType(type) - && field.getAnnotation(DataPoint.class) != null) { - list.add(PotentialAssignment - .forValue(field.getName(), getStaticFieldValue(field))); - } - } - } - } - - private void addArrayValues(String name, List<PotentialAssignment> list, Object array) { - for (int i= 0; i < Array.getLength(array); i++) - list.add(PotentialAssignment.forValue(name + "[" + i + "]", Array.get(array, i))); - } - - private Object getStaticFieldValue(final Field field) { - try { - return field.get(null); - } catch (IllegalArgumentException e) { - throw new RuntimeException( - "unexpected: field from getClass doesn't exist on object"); - } catch (IllegalAccessException e) { - throw new RuntimeException( - "unexpected: getFields returned an inaccessible field"); - } - } + static class MethodParameterValue extends PotentialAssignment { + private final FrameworkMethod method; + + private MethodParameterValue(FrameworkMethod dataPointMethod) { + method = dataPointMethod; + } + + @Override + public Object getValue() throws CouldNotGenerateValueException { + try { + return method.invokeExplosively(null); + } catch (IllegalArgumentException e) { + throw new RuntimeException( + "unexpected: argument length is checked"); + } catch (IllegalAccessException e) { + throw new RuntimeException( + "unexpected: getMethods returned an inaccessible method"); + } catch (Throwable throwable) { + DataPoint annotation = method.getAnnotation(DataPoint.class); + Assume.assumeTrue(annotation == null || !isAssignableToAnyOf(annotation.ignoredExceptions(), throwable)); + + throw new CouldNotGenerateValueException(throwable); + } + } + + @Override + public String getDescription() throws CouldNotGenerateValueException { + return method.getName(); + } + } + + private final TestClass clazz; + + /** + * Constructs a new supplier for {@code type} + */ + public AllMembersSupplier(TestClass type) { + clazz = type; + } + + @Override + public List<PotentialAssignment> getValueSources(ParameterSignature sig) throws Throwable { + List<PotentialAssignment> list = new ArrayList<PotentialAssignment>(); + + addSinglePointFields(sig, list); + addMultiPointFields(sig, list); + addSinglePointMethods(sig, list); + addMultiPointMethods(sig, list); + + return list; + } + + private void addMultiPointMethods(ParameterSignature sig, List<PotentialAssignment> list) throws Throwable { + for (FrameworkMethod dataPointsMethod : getDataPointsMethods(sig)) { + Class<?> returnType = dataPointsMethod.getReturnType(); + + if ((returnType.isArray() && sig.canPotentiallyAcceptType(returnType.getComponentType())) || + Iterable.class.isAssignableFrom(returnType)) { + try { + addDataPointsValues(returnType, sig, dataPointsMethod.getName(), list, + dataPointsMethod.invokeExplosively(null)); + } catch (Throwable throwable) { + DataPoints annotation = dataPointsMethod.getAnnotation(DataPoints.class); + if (annotation != null && isAssignableToAnyOf(annotation.ignoredExceptions(), throwable)) { + return; + } else { + throw throwable; + } + } + } + } + } + + private void addSinglePointMethods(ParameterSignature sig, List<PotentialAssignment> list) { + for (FrameworkMethod dataPointMethod : getSingleDataPointMethods(sig)) { + if (sig.canAcceptType(dataPointMethod.getType())) { + list.add(new MethodParameterValue(dataPointMethod)); + } + } + } + + private void addMultiPointFields(ParameterSignature sig, List<PotentialAssignment> list) { + for (final Field field : getDataPointsFields(sig)) { + Class<?> type = field.getType(); + addDataPointsValues(type, sig, field.getName(), list, getStaticFieldValue(field)); + } + } + + private void addSinglePointFields(ParameterSignature sig, List<PotentialAssignment> list) { + for (final Field field : getSingleDataPointFields(sig)) { + Object value = getStaticFieldValue(field); + + if (sig.canAcceptValue(value)) { + list.add(PotentialAssignment.forValue(field.getName(), value)); + } + } + } + + private void addDataPointsValues(Class<?> type, ParameterSignature sig, String name, + List<PotentialAssignment> list, Object value) { + if (type.isArray()) { + addArrayValues(sig, name, list, value); + } + else if (Iterable.class.isAssignableFrom(type)) { + addIterableValues(sig, name, list, (Iterable<?>) value); + } + } + + private void addArrayValues(ParameterSignature sig, String name, List<PotentialAssignment> list, Object array) { + for (int i = 0; i < Array.getLength(array); i++) { + Object value = Array.get(array, i); + if (sig.canAcceptValue(value)) { + list.add(PotentialAssignment.forValue(name + "[" + i + "]", value)); + } + } + } + + private void addIterableValues(ParameterSignature sig, String name, List<PotentialAssignment> list, Iterable<?> iterable) { + Iterator<?> iterator = iterable.iterator(); + int i = 0; + while (iterator.hasNext()) { + Object value = iterator.next(); + if (sig.canAcceptValue(value)) { + list.add(PotentialAssignment.forValue(name + "[" + i + "]", value)); + } + i += 1; + } + } + + private Object getStaticFieldValue(final Field field) { + try { + return field.get(null); + } catch (IllegalArgumentException e) { + throw new RuntimeException( + "unexpected: field from getClass doesn't exist on object"); + } catch (IllegalAccessException e) { + throw new RuntimeException( + "unexpected: getFields returned an inaccessible field"); + } + } + + private static boolean isAssignableToAnyOf(Class<?>[] typeArray, Object target) { + for (Class<?> type : typeArray) { + if (type.isAssignableFrom(target.getClass())) { + return true; + } + } + return false; + } + + protected Collection<FrameworkMethod> getDataPointsMethods(ParameterSignature sig) { + return clazz.getAnnotatedMethods(DataPoints.class); + } + + protected Collection<Field> getSingleDataPointFields(ParameterSignature sig) { + List<FrameworkField> fields = clazz.getAnnotatedFields(DataPoint.class); + Collection<Field> validFields = new ArrayList<Field>(); + + for (FrameworkField frameworkField : fields) { + validFields.add(frameworkField.getField()); + } + + return validFields; + } + + protected Collection<Field> getDataPointsFields(ParameterSignature sig) { + List<FrameworkField> fields = clazz.getAnnotatedFields(DataPoints.class); + Collection<Field> validFields = new ArrayList<Field>(); + + for (FrameworkField frameworkField : fields) { + validFields.add(frameworkField.getField()); + } + + return validFields; + } + + protected Collection<FrameworkMethod> getSingleDataPointMethods(ParameterSignature sig) { + return clazz.getAnnotatedMethods(DataPoint.class); + } + }
\ No newline at end of file diff --git a/src/main/java/org/junit/experimental/theories/internal/Assignments.java b/src/main/java/org/junit/experimental/theories/internal/Assignments.java index bd94f00..a94c8a5 100644 --- a/src/main/java/org/junit/experimental/theories/internal/Assignments.java +++ b/src/main/java/org/junit/experimental/theories/internal/Assignments.java @@ -1,8 +1,8 @@ -/** - * - */ package org.junit.experimental.theories.internal; +import static java.util.Collections.emptyList; + +import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; @@ -19,115 +19,136 @@ import org.junit.runners.model.TestClass; * parameters */ public class Assignments { - private List<PotentialAssignment> fAssigned; - - private final List<ParameterSignature> fUnassigned; - - private final TestClass fClass; - - private Assignments(List<PotentialAssignment> assigned, - List<ParameterSignature> unassigned, TestClass testClass) { - fUnassigned= unassigned; - fAssigned= assigned; - fClass= testClass; - } - - /** - * Returns a new assignment list for {@code testMethod}, with no params - * assigned. - */ - public static Assignments allUnassigned(Method testMethod, - TestClass testClass) throws Exception { - List<ParameterSignature> signatures; - signatures= ParameterSignature.signatures(testClass - .getOnlyConstructor()); - signatures.addAll(ParameterSignature.signatures(testMethod)); - return new Assignments(new ArrayList<PotentialAssignment>(), - signatures, testClass); - } - - public boolean isComplete() { - return fUnassigned.size() == 0; - } - - public ParameterSignature nextUnassigned() { - return fUnassigned.get(0); - } - - public Assignments assignNext(PotentialAssignment source) { - List<PotentialAssignment> assigned= new ArrayList<PotentialAssignment>( - fAssigned); - assigned.add(source); - - return new Assignments(assigned, fUnassigned.subList(1, fUnassigned - .size()), fClass); - } - - public Object[] getActualValues(int start, int stop, boolean nullsOk) - throws CouldNotGenerateValueException { - Object[] values= new Object[stop - start]; - for (int i= start; i < stop; i++) { - Object value= fAssigned.get(i).getValue(); - if (value == null && !nullsOk) - throw new CouldNotGenerateValueException(); - values[i - start]= value; - } - return values; - } - - public List<PotentialAssignment> potentialsForNextUnassigned() - throws InstantiationException, IllegalAccessException { - ParameterSignature unassigned= nextUnassigned(); - return getSupplier(unassigned).getValueSources(unassigned); - } - - public ParameterSupplier getSupplier(ParameterSignature unassigned) - throws InstantiationException, IllegalAccessException { - ParameterSupplier supplier= getAnnotatedSupplier(unassigned); - if (supplier != null) - return supplier; - - return new AllMembersSupplier(fClass); - } - - public ParameterSupplier getAnnotatedSupplier(ParameterSignature unassigned) - throws InstantiationException, IllegalAccessException { - ParametersSuppliedBy annotation= unassigned - .findDeepAnnotation(ParametersSuppliedBy.class); - if (annotation == null) - return null; - return annotation.value().newInstance(); - } - - public Object[] getConstructorArguments(boolean nullsOk) - throws CouldNotGenerateValueException { - return getActualValues(0, getConstructorParameterCount(), nullsOk); - } - - public Object[] getMethodArguments(boolean nullsOk) - throws CouldNotGenerateValueException { - return getActualValues(getConstructorParameterCount(), - fAssigned.size(), nullsOk); - } - - public Object[] getAllArguments(boolean nullsOk) - throws CouldNotGenerateValueException { - return getActualValues(0, fAssigned.size(), nullsOk); - } - - private int getConstructorParameterCount() { - List<ParameterSignature> signatures= ParameterSignature - .signatures(fClass.getOnlyConstructor()); - int constructorParameterCount= signatures.size(); - return constructorParameterCount; - } - - public Object[] getArgumentStrings(boolean nullsOk) - throws CouldNotGenerateValueException { - Object[] values= new Object[fAssigned.size()]; - for (int i= 0; i < values.length; i++) { - values[i]= fAssigned.get(i).getDescription(); - } - return values; - } + private final List<PotentialAssignment> assigned; + + private final List<ParameterSignature> unassigned; + + private final TestClass clazz; + + private Assignments(List<PotentialAssignment> assigned, + List<ParameterSignature> unassigned, TestClass clazz) { + this.unassigned = unassigned; + this.assigned = assigned; + this.clazz = clazz; + } + + /** + * Returns a new assignment list for {@code testMethod}, with no params + * assigned. + */ + public static Assignments allUnassigned(Method testMethod, + TestClass testClass) { + List<ParameterSignature> signatures; + signatures = ParameterSignature.signatures(testClass + .getOnlyConstructor()); + signatures.addAll(ParameterSignature.signatures(testMethod)); + return new Assignments(new ArrayList<PotentialAssignment>(), + signatures, testClass); + } + + public boolean isComplete() { + return unassigned.size() == 0; + } + + public ParameterSignature nextUnassigned() { + return unassigned.get(0); + } + + public Assignments assignNext(PotentialAssignment source) { + List<PotentialAssignment> assigned = new ArrayList<PotentialAssignment>( + this.assigned); + assigned.add(source); + + return new Assignments(assigned, unassigned.subList(1, + unassigned.size()), clazz); + } + + public Object[] getActualValues(int start, int stop) + throws CouldNotGenerateValueException { + Object[] values = new Object[stop - start]; + for (int i = start; i < stop; i++) { + values[i - start] = assigned.get(i).getValue(); + } + return values; + } + + public List<PotentialAssignment> potentialsForNextUnassigned() + throws Throwable { + ParameterSignature unassigned = nextUnassigned(); + List<PotentialAssignment> assignments = getSupplier(unassigned).getValueSources(unassigned); + + if (assignments.size() == 0) { + assignments = generateAssignmentsFromTypeAlone(unassigned); + } + + return assignments; + } + + private List<PotentialAssignment> generateAssignmentsFromTypeAlone(ParameterSignature unassigned) { + Class<?> paramType = unassigned.getType(); + + if (paramType.isEnum()) { + return new EnumSupplier(paramType).getValueSources(unassigned); + } else if (paramType.equals(Boolean.class) || paramType.equals(boolean.class)) { + return new BooleanSupplier().getValueSources(unassigned); + } else { + return emptyList(); + } + } + + private ParameterSupplier getSupplier(ParameterSignature unassigned) + throws Exception { + ParametersSuppliedBy annotation = unassigned + .findDeepAnnotation(ParametersSuppliedBy.class); + + if (annotation != null) { + return buildParameterSupplierFromClass(annotation.value()); + } else { + return new AllMembersSupplier(clazz); + } + } + + private ParameterSupplier buildParameterSupplierFromClass( + Class<? extends ParameterSupplier> cls) throws Exception { + Constructor<?>[] supplierConstructors = cls.getConstructors(); + + for (Constructor<?> constructor : supplierConstructors) { + Class<?>[] parameterTypes = constructor.getParameterTypes(); + if (parameterTypes.length == 1 + && parameterTypes[0].equals(TestClass.class)) { + return (ParameterSupplier) constructor.newInstance(clazz); + } + } + + return cls.newInstance(); + } + + public Object[] getConstructorArguments() + throws CouldNotGenerateValueException { + return getActualValues(0, getConstructorParameterCount()); + } + + public Object[] getMethodArguments() throws CouldNotGenerateValueException { + return getActualValues(getConstructorParameterCount(), assigned.size()); + } + + public Object[] getAllArguments() throws CouldNotGenerateValueException { + return getActualValues(0, assigned.size()); + } + + private int getConstructorParameterCount() { + List<ParameterSignature> signatures = ParameterSignature + .signatures(clazz.getOnlyConstructor()); + int constructorParameterCount = signatures.size(); + return constructorParameterCount; + } + + public Object[] getArgumentStrings(boolean nullsOk) + throws CouldNotGenerateValueException { + Object[] values = new Object[assigned.size()]; + for (int i = 0; i < values.length; i++) { + values[i] = assigned.get(i).getDescription(); + } + return values; + } }
\ No newline at end of file diff --git a/src/main/java/org/junit/experimental/theories/internal/BooleanSupplier.java b/src/main/java/org/junit/experimental/theories/internal/BooleanSupplier.java new file mode 100644 index 0000000..5f7032f --- /dev/null +++ b/src/main/java/org/junit/experimental/theories/internal/BooleanSupplier.java @@ -0,0 +1,18 @@ +package org.junit.experimental.theories.internal; + +import java.util.Arrays; +import java.util.List; + +import org.junit.experimental.theories.ParameterSignature; +import org.junit.experimental.theories.ParameterSupplier; +import org.junit.experimental.theories.PotentialAssignment; + +public class BooleanSupplier extends ParameterSupplier { + + @Override + public List<PotentialAssignment> getValueSources(ParameterSignature sig) { + return Arrays.asList(PotentialAssignment.forValue("true", true), + PotentialAssignment.forValue("false", false)); + } + +} diff --git a/src/main/java/org/junit/experimental/theories/internal/EnumSupplier.java b/src/main/java/org/junit/experimental/theories/internal/EnumSupplier.java new file mode 100644 index 0000000..1f3ab90 --- /dev/null +++ b/src/main/java/org/junit/experimental/theories/internal/EnumSupplier.java @@ -0,0 +1,30 @@ +package org.junit.experimental.theories.internal; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.experimental.theories.ParameterSignature; +import org.junit.experimental.theories.ParameterSupplier; +import org.junit.experimental.theories.PotentialAssignment; + +public class EnumSupplier extends ParameterSupplier { + + private Class<?> enumType; + + public EnumSupplier(Class<?> enumType) { + this.enumType = enumType; + } + + @Override + public List<PotentialAssignment> getValueSources(ParameterSignature sig) { + Object[] enumValues = enumType.getEnumConstants(); + + List<PotentialAssignment> assignments = new ArrayList<PotentialAssignment>(); + for (Object value : enumValues) { + assignments.add(PotentialAssignment.forValue(value.toString(), value)); + } + + return assignments; + } + +} diff --git a/src/main/java/org/junit/experimental/theories/internal/ParameterizedAssertionError.java b/src/main/java/org/junit/experimental/theories/internal/ParameterizedAssertionError.java index 285bc3a..5b9e947 100644 --- a/src/main/java/org/junit/experimental/theories/internal/ParameterizedAssertionError.java +++ b/src/main/java/org/junit/experimental/theories/internal/ParameterizedAssertionError.java @@ -1,49 +1,50 @@ -/** - * - */ package org.junit.experimental.theories.internal; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; - -public class ParameterizedAssertionError extends RuntimeException { - private static final long serialVersionUID = 1L; - - public ParameterizedAssertionError(Throwable targetException, - String methodName, Object... params) { - super(String.format("%s(%s)", methodName, join(", ", params)), - targetException); - } - - @Override public boolean equals(Object obj) { - return toString().equals(obj.toString()); - } - - public static String join(String delimiter, Object... params) { - return join(delimiter, Arrays.asList(params)); - } - - public static String join(String delimiter, - Collection<Object> values) { - StringBuffer buffer = new StringBuffer(); - Iterator<Object> iter = values.iterator(); - while (iter.hasNext()) { - Object next = iter.next(); - buffer.append(stringValueOf(next)); - if (iter.hasNext()) { - buffer.append(delimiter); - } - } - return buffer.toString(); - } - - private static String stringValueOf(Object next) { - try { - return String.valueOf(next); - } catch (Throwable e) { - return "[toString failed]"; - } - } +public class ParameterizedAssertionError extends AssertionError { + private static final long serialVersionUID = 1L; + + public ParameterizedAssertionError(Throwable targetException, + String methodName, Object... params) { + super(String.format("%s(%s)", methodName, join(", ", params))); + this.initCause(targetException); + } + + @Override + public boolean equals(Object obj) { + return obj instanceof ParameterizedAssertionError && toString().equals(obj.toString()); + } + + @Override + public int hashCode() { + return toString().hashCode(); + } + + public static String join(String delimiter, Object... params) { + return join(delimiter, Arrays.asList(params)); + } + + public static String join(String delimiter, Collection<Object> values) { + StringBuilder sb = new StringBuilder(); + Iterator<Object> iter = values.iterator(); + while (iter.hasNext()) { + Object next = iter.next(); + sb.append(stringValueOf(next)); + if (iter.hasNext()) { + sb.append(delimiter); + } + } + return sb.toString(); + } + + private static String stringValueOf(Object next) { + try { + return String.valueOf(next); + } catch (Throwable e) { + return "[toString failed]"; + } + } }
\ No newline at end of file diff --git a/src/main/java/org/junit/experimental/theories/internal/SpecificDataPointsSupplier.java b/src/main/java/org/junit/experimental/theories/internal/SpecificDataPointsSupplier.java new file mode 100644 index 0000000..7b571e3 --- /dev/null +++ b/src/main/java/org/junit/experimental/theories/internal/SpecificDataPointsSupplier.java @@ -0,0 +1,90 @@ +package org.junit.experimental.theories.internal; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import org.junit.experimental.theories.DataPoint; +import org.junit.experimental.theories.DataPoints; +import org.junit.experimental.theories.FromDataPoints; +import org.junit.experimental.theories.ParameterSignature; +import org.junit.runners.model.FrameworkMethod; +import org.junit.runners.model.TestClass; + +public class SpecificDataPointsSupplier extends AllMembersSupplier { + + public SpecificDataPointsSupplier(TestClass testClass) { + super(testClass); + } + + @Override + protected Collection<Field> getSingleDataPointFields(ParameterSignature sig) { + Collection<Field> fields = super.getSingleDataPointFields(sig); + String requestedName = sig.getAnnotation(FromDataPoints.class).value(); + + List<Field> fieldsWithMatchingNames = new ArrayList<Field>(); + + for (Field field : fields) { + String[] fieldNames = field.getAnnotation(DataPoint.class).value(); + if (Arrays.asList(fieldNames).contains(requestedName)) { + fieldsWithMatchingNames.add(field); + } + } + + return fieldsWithMatchingNames; + } + + @Override + protected Collection<Field> getDataPointsFields(ParameterSignature sig) { + Collection<Field> fields = super.getDataPointsFields(sig); + String requestedName = sig.getAnnotation(FromDataPoints.class).value(); + + List<Field> fieldsWithMatchingNames = new ArrayList<Field>(); + + for (Field field : fields) { + String[] fieldNames = field.getAnnotation(DataPoints.class).value(); + if (Arrays.asList(fieldNames).contains(requestedName)) { + fieldsWithMatchingNames.add(field); + } + } + + return fieldsWithMatchingNames; + } + + @Override + protected Collection<FrameworkMethod> getSingleDataPointMethods(ParameterSignature sig) { + Collection<FrameworkMethod> methods = super.getSingleDataPointMethods(sig); + String requestedName = sig.getAnnotation(FromDataPoints.class).value(); + + List<FrameworkMethod> methodsWithMatchingNames = new ArrayList<FrameworkMethod>(); + + for (FrameworkMethod method : methods) { + String[] methodNames = method.getAnnotation(DataPoint.class).value(); + if (Arrays.asList(methodNames).contains(requestedName)) { + methodsWithMatchingNames.add(method); + } + } + + return methodsWithMatchingNames; + } + + @Override + protected Collection<FrameworkMethod> getDataPointsMethods(ParameterSignature sig) { + Collection<FrameworkMethod> methods = super.getDataPointsMethods(sig); + String requestedName = sig.getAnnotation(FromDataPoints.class).value(); + + List<FrameworkMethod> methodsWithMatchingNames = new ArrayList<FrameworkMethod>(); + + for (FrameworkMethod method : methods) { + String[] methodNames = method.getAnnotation(DataPoints.class).value(); + if (Arrays.asList(methodNames).contains(requestedName)) { + methodsWithMatchingNames.add(method); + } + } + + return methodsWithMatchingNames; + } + +} diff --git a/src/main/java/org/junit/experimental/theories/suppliers/TestedOn.java b/src/main/java/org/junit/experimental/theories/suppliers/TestedOn.java index d6ede64..a19f20a 100644 --- a/src/main/java/org/junit/experimental/theories/suppliers/TestedOn.java +++ b/src/main/java/org/junit/experimental/theories/suppliers/TestedOn.java @@ -1,13 +1,31 @@ package org.junit.experimental.theories.suppliers; +import static java.lang.annotation.ElementType.PARAMETER; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import org.junit.experimental.theories.ParametersSuppliedBy; - +/** + * Annotating a {@link org.junit.experimental.theories.Theory Theory} method int + * parameter with @TestedOn causes it to be supplied with values from the + * ints array given when run as a theory by the + * {@link org.junit.experimental.theories.Theories Theories} runner. For + * example, the below method would be called three times by the Theories runner, + * once with each of the int parameters specified. + * + * <pre> + * @Theory + * public void shouldPassForSomeInts(@TestedOn(ints={1, 2, 3}) int param) { + * ... + * } + * </pre> + */ @ParametersSuppliedBy(TestedOnSupplier.class) @Retention(RetentionPolicy.RUNTIME) +@Target(PARAMETER) public @interface TestedOn { - int[] ints(); + int[] ints(); } diff --git a/src/main/java/org/junit/experimental/theories/suppliers/TestedOnSupplier.java b/src/main/java/org/junit/experimental/theories/suppliers/TestedOnSupplier.java index f80298f..dc3d0c9 100644 --- a/src/main/java/org/junit/experimental/theories/suppliers/TestedOnSupplier.java +++ b/src/main/java/org/junit/experimental/theories/suppliers/TestedOnSupplier.java @@ -1,23 +1,25 @@ package org.junit.experimental.theories.suppliers; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import org.junit.experimental.theories.ParameterSignature; import org.junit.experimental.theories.ParameterSupplier; import org.junit.experimental.theories.PotentialAssignment; - - +/** + * @see org.junit.experimental.theories.suppliers.TestedOn + * @see org.junit.experimental.theories.ParameterSupplier + */ public class TestedOnSupplier extends ParameterSupplier { - @Override public List<PotentialAssignment> getValueSources(ParameterSignature sig) { - List<PotentialAssignment> list = new ArrayList<PotentialAssignment>(); - TestedOn testedOn = sig.getAnnotation(TestedOn.class); - int[] ints = testedOn.ints(); - for (final int i : ints) { - list.add(PotentialAssignment.forValue(Arrays.asList(ints).toString(), i)); - } - return list; - } + @Override + public List<PotentialAssignment> getValueSources(ParameterSignature sig) { + List<PotentialAssignment> list = new ArrayList<PotentialAssignment>(); + TestedOn testedOn = sig.getAnnotation(TestedOn.class); + int[] ints = testedOn.ints(); + for (final int i : ints) { + list.add(PotentialAssignment.forValue("ints", i)); + } + return list; + } } |