/*******************************************************************************
* Copyright (c) 2014 Dawid Pakuła and others. All rights reserved. This program
* and the accompanying materials are made available under the terms of the
* Eclipse Public License v1.0 which accompanies this distribution, and is
* available at http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Dawid Pakuła - initial API and implementation
*******************************************************************************/
package org.eclipse.php.core.tests.runner;
import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.Enumeration;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.eclipse.php.core.PHPVersion;
import org.junit.Test;
import org.junit.runner.Description;
import org.junit.runner.Runner;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.Suite;
import org.junit.runners.model.FrameworkField;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.MultipleFailureException;
import org.junit.runners.model.Statement;
import org.osgi.framework.Bundle;
/**
* Special runner for PDT test with parametters collected to Map<PHPVersion,
* String[] dirs> or String[] dirs
*
* TODO: Allow Before and After with fileName
*/
public class PDTTList extends AbstractPDTTRunner {
/**
* Element for public static field. Have to be Map<PHPVersion, String[]>
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public static @interface Parameters {
/**
* If true, tests will be with real directory structure, each level as
* seperate test tree element
*/
boolean recursive() default false;
}
/**
* Public static method with one or two arguments.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public static @interface BeforeList {
}
/**
* Public static method with one or two arguments
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public static @interface AfterList {
}
/**
* Parent runner for PHPVersionRunner and DirRunner
*/
private abstract class ParentRunner extends Suite {
protected final List<Runner> runners = new LinkedList<Runner>();
protected Object testInstance;
protected String[] fFileList = new String[0];
public ParentRunner(Class<?> klass) throws Exception {
super(klass, EMPTY_RUNNERS);
}
@Override
protected Annotation[] getRunnerAnnotations() {
return new Annotation[0];
}
protected abstract Object[] getConstructorArgs();
protected Object createTestInstance() {
if (testInstance == null) {
try {
testInstance = getTestClass().getOnlyConstructor().newInstance(getConstructorArgs());
} catch (Exception e) {
System.out.println("This is not possible!");
}
}
return testInstance;
}
@Override
protected List<Runner> getChildren() {
return runners;
}
@Override
protected Statement withBeforeClasses(Statement statement) {
List<FrameworkMethod> annotatedMethods = getTestClass().getAnnotatedMethods(BeforeList.class);
if (fFileList.length > 0 && !annotatedMethods.isEmpty()) {
statement = new BeforeStatement(statement, annotatedMethods, createTestInstance());
}
return super.withBeforeClasses(statement);
}
@Override
protected Statement withAfterClasses(Statement statement) {
statement = super.withAfterClasses(statement);
List<FrameworkMethod> annotatedMethods = getTestClass().getAnnotatedMethods(AfterList.class);
if (fFileList.length > 0 && !annotatedMethods.isEmpty()) {
statement = new AfterStatement(statement, annotatedMethods, createTestInstance());
}
return statement;
}
}
/**
* Dir runner is used ony with recursives
*/
private class DirRunner extends ParentRunner {
private final String fTestName;
private final PHPVersion fPHPVersion;
public DirRunner(Class<?> klass, PHPVersion phpVersion, String dir, String testName) throws Throwable {
super(klass);
fTestName = testName;
fPHPVersion = phpVersion;
fFileList = buildFileList(new String[] { dir });
for (String fileName : fFileList) {
runners.add(new FileTestClassRunner(this, fileName));
}
Enumeration<String> entryPaths = getBundle().getEntryPaths(dir);
Bundle bundle = getBundle();
if (entryPaths != null) {
while (entryPaths.hasMoreElements()) {
final String path = (String) entryPaths.nextElement();
if (!path.endsWith("/")) {
continue;
}
final String namePath = path.substring(0, path.length() - 1);
int pos = namePath.lastIndexOf('/');
final String name = (pos >= 0 ? namePath.substring(pos + 1) : namePath);
try {
bundle.getEntry(path); // test Only
runners.add(new DirRunner(klass, phpVersion, path, name));
} catch (Exception e) {
continue;
}
}
}
}
@Override
protected String getName() {
return fTestName;
}
@Override
protected Object[] getConstructorArgs() {
return new Object[] { fPHPVersion, fFileList };
}
}
/**
* PHPVersion group runner, it holds test instance for whole runner
*/
private class PHPVersionRunner extends ParentRunner {
private final PHPVersion fPHPVersion;
public PHPVersionRunner(Class<?> klass, PHPVersion phpVersion, String[] dirs) throws Throwable {
super(klass);
fPHPVersion = phpVersion;
if (isRecursive) {
for (String dirName : dirs) {
runners.add(new DirRunner(klass, phpVersion, dirName, dirName));
}
} else {
fFileList = buildFileList(dirs);
for (String fileName : fFileList) {
runners.add(new FileTestClassRunner(this, fileName));
}
}
}
@Override
protected String getName() {
return fPHPVersion.toString();
}
@Override
protected Statement classBlock(RunNotifier notifier) {
Statement statement = childrenInvoker(notifier);
if (!isRecursive) {
statement = withBeforeClasses(statement);
statement = withAfterClasses(statement);
}
return statement;
}
protected void collectInitializationErrors(java.util.List<Throwable> errors) {
super.collectInitializationErrors(errors);
validatePublicVoidWithNoArguments(BeforeList.class, errors);
validatePublicVoidWithNoArguments(AfterList.class, errors);
}
protected void validatePublicVoidWithNoArguments(Class<? extends Annotation> clazz, List<Throwable> errors) {
for (FrameworkMethod method : getTestClass().getAnnotatedMethods(clazz)) {
if (!method.isPublic() || method.isStatic()) {
errors.add(new Exception("Method have to be public not static"));
}
if (method.getMethod().getParameterTypes().length != 0) {
errors.add(new Exception("Method must be without arguments"));
}
}
}
@Override
protected Object[] getConstructorArgs() {
return new Object[] { fPHPVersion, fFileList };
}
}
public class BeforeStatement extends Statement {
private final Statement fNext;
private final List<FrameworkMethod> fBefores;
private final Object fTest;
public BeforeStatement(Statement next, List<FrameworkMethod> befores, Object test) {
fNext = next;
fBefores = befores;
fTest = test;
}
@Override
public void evaluate() throws Throwable {
for (FrameworkMethod before : fBefores) {
before.invokeExplosively(fTest);
}
fNext.evaluate();
}
}
public class AfterStatement extends Statement {
private final Statement fBefore;
private final List<FrameworkMethod> fAfters;
private final Object fTest;
public AfterStatement(Statement before, List<FrameworkMethod> afters, Object test) {
fBefore = before;
fAfters = afters;
fTest = test;
}
@Override
public void evaluate() throws Throwable {
List<Throwable> errors = new LinkedList<Throwable>();
try {
fBefore.evaluate();
} catch (Throwable e) {
errors.add(e);
} finally {
for (FrameworkMethod after : fAfters) {
try {
after.invokeExplosively(fTest);
} catch (Throwable e) {
errors.add(e);
}
}
}
MultipleFailureException.assertEmpty(errors);
}
}
/**
* Runner for concrette PDTT file, it also holds TestInstance
*/
private class FileTestClassRunner extends BlockJUnit4ClassRunner {
private final String fName;
private final int fIndex;
private final ParentRunner fParentRunner;
public FileTestClassRunner(ParentRunner parentRunner, String name) throws InitializationError {
super(parentRunner.getTestClass().getJavaClass());
fName = name;
// to avoid jUnit limitation
fIndex = counter++;
fParentRunner = parentRunner;
}
@Override
protected String getName() {
return fName;
}
@Override
protected String testName(FrameworkMethod method) {
return method.getName() + '[' + fIndex + ']';
}
@Override
protected void validateConstructor(List<Throwable> errors) {
Class<?> javaClass = getTestClass().getJavaClass();
if (javaClass.getConstructors().length != 1) {
errors.add(new Exception("Only one constructor is allowed!"));
return;
}
Constructor<?> constructor = javaClass.getConstructors()[0];
if (constructor.getParameterTypes().length != 2
|| !constructor.getParameterTypes()[0].isAssignableFrom(PHPVersion.class)
|| !constructor.getParameterTypes()[1].isAssignableFrom(String[].class)) {
errors.add(new Exception("Public constructor with phpVersion and String[] argument is required"));
}
}
@Override
protected Object createTest() throws Exception {
return fParentRunner.createTestInstance();
}
@Override
protected Statement classBlock(RunNotifier notifier) {
return childrenInvoker(notifier);
}
@Override
protected Annotation[] getRunnerAnnotations() {
return new Annotation[0];
}
@Override
protected void validateTestMethods(List<Throwable> errors) {
for (FrameworkMethod method : getTestClass().getAnnotatedMethods(Test.class)) {
method.validatePublicVoid(false, errors);
Class<?>[] types = method.getMethod().getParameterTypes();
if (!(types.length == 0 || (types.length == 1 && types[0].isAssignableFrom(String.class)))) {
errors.add(new Exception(method.toString() + ": Dirs list must by empty or one string"));
}
}
}
@Override
protected Statement methodInvoker(final FrameworkMethod method, final Object test) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
if (method.getMethod().getParameterTypes().length == 0) {
method.invokeExplosively(test);
} else {
method.invokeExplosively(test, fName);
}
}
};
}
@Override
protected Description describeChild(FrameworkMethod method) {
return Description.createTestDescription(getTestClass().getName(), method.getName() + '[' + fName + ']',
getTestClass().getName() + '#' + fName + fIndex);
}
}
private class SimpleFileRunner extends FileTestClassRunner {
public SimpleFileRunner(ParentRunner parentRunner, String name) throws InitializationError {
super(parentRunner, name);
}
@Override
protected void validateConstructor(List<Throwable> errors) {
Class<?> javaClass = getTestClass().getJavaClass();
if (javaClass.getConstructors().length != 1) {
errors.add(new Exception("Only one constructor is allowed!"));
return;
}
Constructor<?> constructor = javaClass.getConstructors()[0];
if (constructor.getParameterTypes().length != 1
|| !constructor.getParameterTypes()[0].isAssignableFrom(String[].class)) {
errors.add(new Exception("Public constructor with String[] argument is required"));
}
}
}
private Map<PHPVersion, String[]> parameters;
private String[] dirs;
private boolean isRecursive = false;
private boolean isArray = false;
private FakeFlatRunner flatRunner = null;
public PDTTList(Class<?> klass) throws Throwable {
super(klass);
readParameters();
if (isArray) {
buildFlatRunners();
} else {
buildRunners();
}
}
private class FakeFlatRunner extends ParentRunner {
public FakeFlatRunner(Class<?> klass, String[] fileList) throws Exception {
super(klass);
fFileList = fileList;
}
@Override
protected Object[] getConstructorArgs() {
return new Object[] { fFileList };
}
}
private void buildFlatRunners() throws Throwable {
String[] fileList = buildFileList(dirs);
flatRunner = new FakeFlatRunner(getTestClass().getJavaClass(), fileList);
for (String fName : fileList) {
runners.add(new SimpleFileRunner(flatRunner, fName));
}
}
private void buildRunners() throws Throwable {
for (Entry<PHPVersion, String[]> entry : parameters.entrySet()) {
runners.add(new PHPVersionRunner(getTestClass().getJavaClass(), entry.getKey(), entry.getValue()));
}
}
private void readParameters() throws Exception {
for (FrameworkField field : getTestClass().getAnnotatedFields(Parameters.class)) {
if (field.isPublic() && field.isPublic()) {
if (field.getType().isAssignableFrom(Map.class)) {
parameters = (Map<PHPVersion, String[]>) field.getField().get(null);
} else if (field.getType().isAssignableFrom(String[].class)) {
dirs = (String[]) field.getField().get(null);
isArray = true;
} else {
continue;
}
for (Annotation ann : field.getAnnotations()) {
if (ann instanceof Parameters) {
isRecursive = ((Parameters) ann).recursive();
}
}
return;
}
}
throw new Exception(getTestClass().getName()
+ ": Public static Map<PHPVersion, String[]>|String[] field with @Parameters is required");
}
@Override
protected Statement withAfterClasses(Statement statement) {
if (flatRunner != null) {
return flatRunner.withAfterClasses(statement);
}
return super.withAfterClasses(statement);
}
@Override
protected Statement withBeforeClasses(Statement statement) {
if (flatRunner != null) {
return flatRunner.withBeforeClasses(statement);
}
return super.withBeforeClasses(statement);
}
}