/*
* Copyright 2004-2015 the Seasar Foundation and the Others.
*
* 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.seasar.framework.unit.impl;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.Test.None;
import org.seasar.framework.aop.Pointcut;
import org.seasar.framework.aop.interceptors.MockInterceptor;
import org.seasar.framework.container.AspectDef;
import org.seasar.framework.container.factory.AspectDefFactory;
import org.seasar.framework.env.Env;
import org.seasar.framework.unit.Expression;
import org.seasar.framework.unit.InternalTestContext;
import org.seasar.framework.unit.S2TestIntrospector;
import org.seasar.framework.unit.annotation.Mock;
import org.seasar.framework.unit.annotation.Mocks;
import org.seasar.framework.unit.annotation.PostBindFields;
import org.seasar.framework.unit.annotation.PreUnbindFields;
import org.seasar.framework.unit.annotation.Prerequisite;
import org.seasar.framework.unit.annotation.RegisterNamingConvention;
import org.seasar.framework.unit.annotation.RootDicon;
import org.seasar.framework.unit.annotation.TxBehavior;
import org.seasar.framework.unit.annotation.TxBehaviorType;
import org.seasar.framework.unit.annotation.WarmDeploy;
import org.seasar.framework.util.StringUtil;
import org.seasar.framework.util.tiger.CollectionsUtil;
/**
* アノテーションを解釈してテストクラスを分析するイントロスペクターです。
*
* @author taedium
*/
public class AnnotationTestIntrospector implements S2TestIntrospector {
/** テストクラスの初期化メソッドに注釈可能なアノテーションクラス */
protected Class<? extends Annotation> beforeClassAnnotation = BeforeClass.class;
/** テストクラスの解放メソッドに注釈可能なアノテーションクラス */
protected Class<? extends Annotation> afterClassAnnotation = AfterClass.class;
/** 全テストケースに共通の初期化メソッドに注釈可能なアノテーションクラス */
protected Class<? extends Annotation> beforeAnnotation = Before.class;
/** 全テストケースに共通の解放メソッドに注釈可能なアノテーションクラス */
protected Class<? extends Annotation> afterAnnotation = After.class;
/** テストクラスのバインドフィールド直後のメソッドに注釈可能なアノテーションクラス */
protected Class<? extends Annotation> postBindFieldsAnnotation = PostBindFields.class;
/** テストクラスのアンバインドフィールド直前のメソッドメソッドに注釈可能なアノテーションクラス */
protected Class<? extends Annotation> preUnbindFieldsAnnotation = PreUnbindFields.class;
/** テストケースを無視する処理が有効かどうかを表すフラグ。デフォルトは<code>true</code> */
protected boolean enableIgnore = true;
/** テストケースの事前条件チェック処理が有効かどうかを表すフラグ。デフォルトは<code>true</code> */
protected boolean enablePrerequisite = true;
/**
* テストクラスの初期化メソッドに注釈可能なアノテーションクラスを設定します。
*
* @param beforeClassAnnotation
* アノテーションクラス
*/
public void setBeforeClassAnnotation(
final Class<? extends Annotation> beforeClassAnnotation) {
this.beforeClassAnnotation = beforeClassAnnotation;
}
/**
* テストクラスの解放メソッドに注釈可能なアノテーションクラスを設定します。
*
* @param afterClassAnnotation
* アノテーションクラス
*/
public void setAfterClassAnnotation(
final Class<? extends Annotation> afterClassAnnotation) {
this.afterClassAnnotation = afterClassAnnotation;
}
/**
* 全テストケースに共通の初期化メソッドに注釈可能なアノテーションクラスを設定します。
*
* @param beforeAnnotation
* アノテーションクラス
*/
public void setBeforeAnnotation(
final Class<? extends Annotation> beforeAnnotation) {
this.beforeAnnotation = beforeAnnotation;
}
/**
* 全テストケースに共通の初期化メソッドに注釈可能なアノテーションクラスを設定します。
*
* @param afterAnnotation
* アノテーションクラス
*/
public void setAfterAnnotation(
final Class<? extends Annotation> afterAnnotation) {
this.afterAnnotation = afterAnnotation;
}
/**
* テストクラスのバインドフィールド直後のメソッドに注釈可能なアノテーションクラスを設定します。
*
* @param postBindFieldsAnnotation
* アノテーションクラス
*/
public void setPostBindFieldsAnnotation(
Class<? extends Annotation> postBindFieldsAnnotation) {
this.postBindFieldsAnnotation = postBindFieldsAnnotation;
}
/**
* テストクラスのアンバインドフィールド直前のメソッドに注釈可能なアノテーションクラスを設定します。
*
* @param preUnbindFieldsAnnotation
* アノテーションクラス
*/
public void setPreUnbindFieldsAnnotation(
Class<? extends Annotation> preUnbindFieldsAnnotation) {
this.preUnbindFieldsAnnotation = preUnbindFieldsAnnotation;
}
/**
* テストケースを無視する処理を有効とするかどうかを設定します。
*
* @param enableIgnore
* 無視する処理が有効の場合<code>true</code>、そうでない場合<code>false</code>
*/
public void setEnableIgnore(boolean enableIgnore) {
this.enableIgnore = enableIgnore;
}
/**
* テストケースの事前条件チェックの処理を有効とするかどうかを設定します。
*
* @param enablePrerequisite
* 事前条件チェックの処理が有効の場合<code>true</code>、そうでない場合<code>false</code>
*/
public void setEnablePrerequisite(boolean enablePrerequisite) {
this.enablePrerequisite = enablePrerequisite;
}
public List<Method> getBeforeClassMethods(final Class<?> clazz) {
return getAnnotatedMethods(clazz, beforeClassAnnotation);
}
public List<Method> getAfterClassMethods(final Class<?> clazz) {
return getAnnotatedMethods(clazz, afterClassAnnotation);
}
public List<Method> getPostBindFieldsMethods(final Class<?> clazz) {
return getAnnotatedMethods(clazz, postBindFieldsAnnotation);
}
public List<Method> getPreUnbindFieldsMethods(final Class<?> clazz) {
return getAnnotatedMethods(clazz, preUnbindFieldsAnnotation);
}
public List<Method> getBeforeMethods(final Class<?> clazz) {
return getAnnotatedMethods(clazz, beforeAnnotation);
}
public List<Method> getAfterMethods(final Class<?> clazz) {
return getAnnotatedMethods(clazz, afterAnnotation);
}
public List<Method> getTestMethods(final Class<?> clazz) {
return getAnnotatedMethods(clazz, Test.class);
}
/**
* アノテーションが付与されたメソッドのリストを返します。
*
* @param clazz
* テストクラス
* @param annotationClass
* アノテーションクラス
* @return アノテーションが付与されたメソッドのリスト
*/
protected List<Method> getAnnotatedMethods(Class<?> clazz,
Class<? extends Annotation> annotationClass) {
List<Method> results = IntrospectorUtil.getAnnotatedMethods(clazz,
annotationClass);
if (runsTopToBottom(annotationClass))
Collections.reverse(results);
return results;
}
/**
* クラスの階層構造の上位から実行するならば<code>true</code>を返します。
*
* @param annotation
* アノテーション
* @return クラスの階層構造の上位から実行するならば<code>true</code>
*/
protected boolean runsTopToBottom(Class<? extends Annotation> annotation) {
return annotation.equals(Before.class)
|| annotation.equals(BeforeClass.class);
}
public Method getEachBeforeMethod(final Class<?> clazz, final Method method) {
return null;
}
public Method getEachAfterMethod(final Class<?> clazz, final Method method) {
return null;
}
public Method getEachRecordMethod(final Class<?> clazz, final Method method) {
return null;
}
public Class<? extends Throwable> expectedException(final Method method) {
final Test annotation = method.getAnnotation(Test.class);
if (annotation == null || annotation.expected() == None.class) {
return null;
}
return annotation.expected();
}
public long getTimeout(final Method method) {
final Test annotation = method.getAnnotation(Test.class);
if (annotation != null) {
return annotation.timeout();
}
return 0;
}
public boolean isIgnored(final Method method) {
if (enableIgnore) {
return method.isAnnotationPresent(Ignore.class);
}
return false;
}
public boolean isFulfilled(final Class<?> clazz, final Method method,
final Object test) {
if (!enablePrerequisite) {
return true;
}
if (clazz.isAnnotationPresent(Prerequisite.class)) {
final String source = clazz.getAnnotation(Prerequisite.class)
.value();
final Expression exp = createExpression(source, method, test);
if (!isFulfilled(exp)) {
return false;
}
}
if (method.isAnnotationPresent(Prerequisite.class)) {
final String source = method.getAnnotation(Prerequisite.class)
.value();
final Expression exp = createExpression(source, method, test);
if (!isFulfilled(exp)) {
return false;
}
}
return true;
}
/**
* 事前条件が満たされた場合<code>true</code>を返します。
*
* @param expression
* 事前条件を表す式
* @return 事前条件が満たされた場合<code>true</code>、満たされない場合<code>false</code>
*/
protected boolean isFulfilled(final Expression expression) {
final Object result = expression.evaluateNoException();
if (expression.isMethodFailed()) {
System.err.println(expression.getException());
return false;
}
expression.throwExceptionIfNecessary();
if (result instanceof Boolean && Boolean.class.cast(result)) {
return true;
}
return false;
}
public boolean needsTransaction(final Class<?> clazz, final Method method) {
final TxBehaviorType type = getTxBehaviorType(clazz, method);
return type == null || type != TxBehaviorType.NONE;
}
public boolean requiresTransactionCommitment(final Class<?> clazz,
final Method method) {
final TxBehaviorType type = getTxBehaviorType(clazz, method);
return type != null && type == TxBehaviorType.COMMIT;
}
/**
* トランザクションの振る舞いを返します。
*
* @param clazz
* テストクラス
* @param method
* テストメソッド
* @return トランザクションの振る舞い
*/
protected TxBehaviorType getTxBehaviorType(final Class<?> clazz,
final Method method) {
if (method.isAnnotationPresent(TxBehavior.class)) {
return method.getAnnotation(TxBehavior.class).value();
}
if (clazz.isAnnotationPresent(TxBehavior.class)) {
return clazz.getAnnotation(TxBehavior.class).value();
}
return null;
}
public boolean needsWarmDeploy(final Class<?> clazz, final Method method) {
if (method.isAnnotationPresent(WarmDeploy.class)) {
return method.getAnnotation(WarmDeploy.class).value();
}
if (clazz.isAnnotationPresent(WarmDeploy.class)) {
return clazz.getAnnotation(WarmDeploy.class).value();
}
return true;
}
public boolean isRegisterNamingConvention(final Class<?> clazz,
final Method method) {
if (method.isAnnotationPresent(RegisterNamingConvention.class)) {
return method.getAnnotation(RegisterNamingConvention.class).value();
}
if (clazz.isAnnotationPresent(RegisterNamingConvention.class)) {
return clazz.getAnnotation(RegisterNamingConvention.class).value();
}
return true;
}
public void createMock(final Method method, final Object test,
final InternalTestContext context) {
final Mock mock = method.getAnnotation(Mock.class);
if (mock != null) {
createMock(mock, method, test, context);
} else {
final Mocks mocks = method.getAnnotation(Mocks.class);
if (mocks != null) {
for (final Mock each : mocks.value()) {
createMock(each, method, test, context);
}
}
}
}
/**
* <code>mock</code>から{@link MockInterceptor モックインターセプター}を作成し、<code>context</code>に登録します。
*
* @param mock
* モックインターセプターの定義
* @param method
* テストメソッド
* @param test
* テストクラスのインスタンス
* @param context
* S2JUnit4の内部的なテストコンテキスト
*/
protected void createMock(final Mock mock, final Method method,
final Object test, final InternalTestContext context) {
final MockInterceptor mi = new MockInterceptor();
if (!StringUtil.isEmpty(mock.returnValue())) {
final Expression exp = createExpression(mock.returnValue(), method,
test);
mi.setReturnValue(exp.evaluate());
}
if (!StringUtil.isEmpty(mock.throwable())) {
final Expression exp = createExpression(mock.throwable(), method,
test);
final Object result = exp.evaluate();
mi.setThrowable(Throwable.class.cast(result));
}
Pointcut pc = null;
if (StringUtil.isEmpty(mock.pointcut())) {
pc = AspectDefFactory.createPointcut(mock.target());
} else {
pc = AspectDefFactory.createPointcut(mock.pointcut());
}
final Object componentKey = StringUtil.isEmpty(mock.targetName()) ? mock
.target()
: mock.targetName();
final AspectDef aspectDef = AspectDefFactory.createAspectDef(mi, pc);
context.addAspecDef(componentKey, aspectDef);
context.addMockInterceptor(mi);
}
/**
* 式を作成します。
*
* @param source
* 式の文字列表現
* @param method
* テストメソッド
* @param test
* テストクラスのインスタンス
* @return 式
*/
protected Expression createExpression(final String source,
final Method method, final Object test) {
final Map<String, Object> ctx = CollectionsUtil.newHashMap();
ctx.put("ENV", Env.getValue());
ctx.put("method", method);
return new OgnlExpression(source, test, ctx);
}
public String getRootDicon(final Class<?> clazz, final Method method) {
if (method.isAnnotationPresent(RootDicon.class)) {
return method.getAnnotation(RootDicon.class).value();
}
if (clazz.isAnnotationPresent(RootDicon.class)) {
return clazz.getAnnotation(RootDicon.class).value();
}
return null;
}
}