/*
* Copyright 2010-2012 VMware and contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springsource.loaded.testgen;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.junit.Assert;
import org.junit.runner.RunWith;
import org.springsource.loaded.ri.ReflectiveInterceptor;
import org.springsource.loaded.test.infra.Result;
/**
* This class is intended to be subclassed to create 'generated' sprongloaded tests. It needs to be run with the
* {@link ExploreAllChoicesRunner} test runner, using the {@link RunWith} annotation.
* <p>
* To create a generative test two things come together:
*
* <ul>
* <li>A mechanism to create different test configurations based on 'random' choices. These random choices are made by
* the test's 'setup' method calling the provided 'choice' methods.
*
* <li>A mechanism to run the same test twice in two different execution contexts. It is the responsibility of the test
* subclass. One context uses an ordinary Java classloader to obtain Class objects by loading a the 'final' version of
* the class. The other uses SpringLoaded infrastructure, and reloads a class's successive version up to the 'final'
* version.
* </ul>
*
* On a first run, the test runner will provide a 'recording' random choices generator and an 'ordinary Java context'.
* The test is run multiple times until all possible choices have been explored. for each test run the choices are
* recorded together with the observed test result. This used to populate the test tree.
* <p>
* Then the tests are run again replaying the recorded choices, in a SpringLoaded context. The result is compared with
* the result from the first run. The test fails if the results are not 'equals'.
*
* @author kdvolder
*/
public abstract class GenerativeSpringLoadedTest extends GenerativeTest {
/**
* Provides the execution context where we can load class versions, so that we can then uses these classes to
* execute reflective calls on them.
* <p>
* During 'generative' setup, an execution context with a standard java class loader is used to 'predict' the
* expected test result. During 'replay' a SpringLoaded based implementation is used instead.
*/
protected IClassProvider classProvider = null;
/**
* To have 'nice' toString value and display name. While generating test parameters, add some text to this buffer to
* describe the selected parameter. Method in this class that are called 'targetXXX' generally will add some text to
* this buffer.
*/
protected StringBuffer toStringValue = new StringBuffer();
/**
* Typically, each test has a particular 'target package' which is a package in the test data project that contains
* the reloadable classes that this test is operating on. This method must be implemented by the subclass to provide
* a suitable value.
*/
protected abstract String getTargetPackage();
/**
* Loads up a given version of a given type.
* <p>
* In 'JustJava' mode a standard Java classloader is used to immediately load the stipulated version.
* <p>
* In 'SpringLoaded' mode the original version of the type is loaded first. Then successive version are reloaded
* until the stipulated version number is reached.
* <p>
* This method should only be called to load a class that has not been loaded yet or an error will occur.
* <p>
* Since loading a class may trigger loading of dependent classes, it is important to call this method on classes in
* the correct order.
*
* @param typeName Dotted name of the type to load.
* @param version One of "", "002", "003", ...
*/
protected Class<?> loadClassVersion(String typeName, String version) {
return classProvider.loadClassVersion(typeName, version);
}
/**
* Get a type from the classloader. Use this to get references to already loaded classes, or to get classes that
* fall outside the reloadable types universe.
*/
public Class<?> classForName(String className) throws ClassNotFoundException {
return classProvider.classForName(className);
}
@Override
public void setup() throws Exception, RejectedChoice {
super.setup();
if (generative) {
classProvider = new JustJavaClassProvider();
}
else {
classProvider = new SpringLoadedClassProvider(getReloableTypeConfig());
}
chooseTestParameters();
}
/**
* This method should be overridden in order for a test to choose its test parameters. If a test throws
* RejectedChoice exception, this test configuration will be silently ignored. Any other exceptions raised will
* result in an error initialising the test suite.
*
* @throws RejectedChoice
* @throws Exception
*/
protected abstract void chooseTestParameters() throws RejectedChoice, Exception;
/**
* Override this in your own test class to configure SpringLoaded type registry.
*/
protected String getReloableTypeConfig() {
String targetPackage = getTargetPackage();
Assert.assertNotNull(targetPackage);
return targetPackage + "..*";
}
/**
* Select a method from given class's declared methods as a test target.
*
* @throws RejectedChoice If class has no methods to choose from.
*/
protected Method targetMethodFrom(Class<?> targetClass) throws RejectedChoice {
Method[] methods = ReflectiveInterceptor.jlClassGetDeclaredMethods(targetClass);
//To be deterministic we must sort these methods in a predictable fashion! Otherwise the test
//may compare results from one method in the first run with those of another method in the second
//run and fail.
sort(methods);
// Arrays.sort(methods, new ToStringComparator());
Method method = choice(methods);
toStringValue.append(method);
return method;
}
/**
* Select a field from given class's declared field as a test target.
*
* @throws RejectedChoice If class has no fields to choose from.
*/
protected Field targetFieldFrom(Class<?> clazz, FieldGetMethod howToGet) throws RejectedChoice {
Field[] fields = null;
switch (howToGet) {
case GET_DECLARED_FIELD:
case GET_DECLARED_FIELDS:
fields = ReflectiveInterceptor.jlClassGetDeclaredFields(clazz);
break;
case GET_FIELD:
case GET_FIELDS:
fields = ReflectiveInterceptor.jlClassGetFields(clazz);
break;
}
//To be deterministic we must sort these in a predictable fashion!
// Arrays.sort(fields, new ToStringComparator());
sort(fields);
Field f = choice(fields);
toStringValue.append(f.getName());
try {
switch (howToGet) {
case GET_DECLARED_FIELDS:
case GET_FIELDS:
return f;
case GET_DECLARED_FIELD:
return ReflectiveInterceptor.jlClassGetDeclaredField(clazz, f.getName());
case GET_FIELD:
return ReflectiveInterceptor.jlClassGetField(clazz, f.getName());
}
}
catch (Exception e) {
throw new Error(e);
}
return f;
}
/**
* Select a Constructor from given class's declared constructors as a test target.
*
* @throws RejectedChoice If class has no Constructors to choose from.
*/
protected Constructor<?> targetConstructorFrom(Class<?> clazz) throws RejectedChoice {
Constructor<?>[] constructors = ReflectiveInterceptor.jlClassGetDeclaredConstructors(clazz);
//To be deterministic we must sort these methods in a predictable fashion!
//Arrays.sort(constructors, new ToStringComparator());
sort(constructors);
Constructor<?> c = choice(constructors);
toStringValue.append(c);
return c;
}
/**
* Load and selected a target type for testing. This adds the name of the class to the 'toStringValue' making it
* part of the test description.
*/
protected Class<?> targetClass(String typeName, String version) {
toStringValue.append(typeName + version + " ");
return loadClassVersion(getTargetPackage() + "." + typeName, version);
}
/**
* Similar to other targetClass method, but for getting an unversioned, non-reloadable type.
* <p>
* Typically this class will <b>not</b> be in the target package (since it is non-reloadable) so you must explicitly
* include the package name in the type name.
*/
protected Class<?> targetClass(String fullyQuallifiedName) throws ClassNotFoundException {
if (fullyQuallifiedName == null) {
toStringValue.append("null");
return null;
}
else {
Class<?> clazz = classForName(fullyQuallifiedName);
toStringValue.append(clazz.getSimpleName() + " ");
return clazz;
}
}
@Override
public String getConfigDescription() {
return toStringValue.toString();
}
@Override
public String toString() {
return this.getClass().getSimpleName() + " " + getConfigDescription();
}
/**
* Most of the stuff we are interested in (Methods, Classes, Annotations, will not be 'equals') when they are
* executed in a different classloader. So we approximate this by just calling 'toString' on returned objects and
* comparing those.
*/
@Override
protected void assertEqualResults(Result expected, Result actual) {
Assert.assertEquals("" + expected.returnValue, "" + actual.returnValue);
}
/**
* If your test is expected to return a list of stuff that may not be 'equals' to each other because
* <ul>
* <li>objects come from different classloaders and are not equals
* <li>the order of the objects may vary
* </ul>
* Then, assuming the objects have a reasonable toString implementation, you can use this method as an
* implementation of assertEqualResults. Simply call this method from your assertEqualResults method.
*/
protected void assertEqualUnorderedToStringLists(Result _expected, Result _actual) {
List<String> expected = toStringList((List<?>) _expected.returnValue);
List<String> actual = toStringList((List<?>) _actual.returnValue);
StringBuffer msg = new StringBuffer("Actual " + actual + " don't match expected " + expected + "\n");
List<String> extra = new ArrayList<String>(actual);
extra.removeAll(expected);
if (!extra.isEmpty()) {
msg.append("extra: \n");
for (String string : extra) {
msg.append(" " + string + "\n");
}
}
List<String> missing = new ArrayList<String>(expected);
missing.removeAll(actual);
if (!missing.isEmpty()) {
msg.append("missing: \n");
for (String string : missing) {
msg.append(" " + string + "\n");
}
}
Assert.assertTrue(msg.toString(), missing.isEmpty() && extra.isEmpty());
Assert.assertEquals("Duplicates in result?", expected.size(), actual.size());
}
/**
* Converts a list of any type of object into a list of Strings by calling the toString method on each object.
*/
protected List<String> toStringList(List<?> list) {
List<String> result = new ArrayList<String>();
for (Object obj : list) {
result.add("" + obj);
}
return result;
}
protected void sort(Object[] os) {
for (int i = 0; i < os.length; i++) {
for (int x = 1; x < os.length - i; x++) {
if (os[x - 1].toString().compareTo(os[x].toString()) > 0) {
Object temp = os[x - 1];
os[x - 1] = os[x];
os[x] = temp;
}
}
}
}
}