Created
August 5, 2008 05:12
-
-
Save tomjadams/4030 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright 2006-2008 Workingmouse | |
* | |
* 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 com.googlecode.instinct.internal.runner; | |
import com.googlecode.instinct.internal.core.ContextClass; | |
import com.googlecode.instinct.internal.core.LifecycleMethod; | |
import com.googlecode.instinct.internal.core.SpecificationMethod; | |
import com.googlecode.instinct.internal.reflect.ConstructorInvoker; | |
import com.googlecode.instinct.internal.reflect.ConstructorInvokerImpl; | |
import static com.googlecode.instinct.internal.runner.SpecificationRunSuccessStatus.SPECIFICATION_SUCCESS; | |
import com.googlecode.instinct.internal.util.AggregatingException; | |
import com.googlecode.instinct.internal.util.Clock; | |
import com.googlecode.instinct.internal.util.ClockImpl; | |
import static com.googlecode.instinct.internal.util.ParamChecker.checkNotNull; | |
import com.googlecode.instinct.internal.util.Suggest; | |
import com.googlecode.instinct.internal.util.exception.ExceptionFinder; | |
import com.googlecode.instinct.internal.util.exception.ExceptionFinderImpl; | |
import com.googlecode.instinct.marker.annotate.Context; | |
import com.googlecode.instinct.runner.SpecificationLifecycle; | |
import com.googlecode.instinct.runner.StandardSpecificationLifecycle; | |
import fj.F; | |
import fj.F2; | |
import fj.F3; | |
import fj.Unit; | |
import static fj.Unit.unit; | |
import fj.data.Either; | |
import fj.data.List; | |
import fj.data.NonEmptyList; | |
import fj.data.Option; | |
import static fj.data.Option.some; | |
import static fj.data.Option.somes; | |
import fj.data.Validation; | |
import static fj.data.Validation.validation; | |
import fj.pre.Semigroup; | |
import java.lang.reflect.AnnotatedElement; | |
public final class SpecificationRunnerImpl implements SpecificationRunner { | |
private final ConstructorInvoker constructorInvoker = new ConstructorInvokerImpl(); | |
private final LifeCycleMethodValidator methodValidator = new LifeCycleMethodValidatorImpl(); | |
private final ExceptionFinder exceptionFinder = new ExceptionFinderImpl(); | |
private final Clock clock = new ClockImpl(); | |
public SpecificationResult run(final SpecificationMethod specificationMethod) { | |
checkNotNull(specificationMethod); | |
final long startTime = clock.getCurrentTime(); | |
final SpecificationLifecycle lifecycle = lifecycle(specificationMethod.getContextClass()); | |
return runLifecycle(startTime, lifecycle, specificationMethod); | |
} | |
private SpecificationResult runLifecycle(final long startTime, final SpecificationLifecycle lifecycle, | |
final SpecificationMethod specificationMethod) { | |
final Either<Throwable, ContextClass> createContext = lifecycle.createContext(specificationMethod.getContextClass()); | |
if (createContext.isLeft()) { | |
return fail(startTime, specificationMethod, createContext.left().value(), Option.<Throwable>none()); | |
} else { | |
final ContextClass contextClass = createContext.right().value(); | |
final Validation<Throwable, Unit> validation = validateSpecification(contextClass, specificationMethod); | |
if (validation.isFail()) { | |
return fail(startTime, specificationMethod, validation.fail(), Option.<Throwable>none()); | |
} else { | |
return runSpecification(startTime, lifecycle, contextClass, specificationMethod); | |
} | |
} | |
} | |
private SpecificationResult runSpecification(final long startTime, final SpecificationLifecycle lifecycle, final ContextClass contextClass, | |
final SpecificationMethod specificationMethod) { | |
final Object contextInstance = constructorInvoker.invokeNullaryConstructor(contextClass.getType()); | |
final Validation<Throwable, Unit> preSpecificationSteps = | |
validate(resetMocks().f(lifecycle)).sequence(validation(wireActors().f(lifecycle, contextInstance))) | |
.sequence(validate(befores().f(lifecycle, contextInstance, contextClass.getBeforeSpecificationMethods()))); | |
if (preSpecificationSteps.isFail()) { | |
return fail(startTime, specificationMethod, preSpecificationSteps.fail(), Option.<Throwable>none()); | |
} else { | |
final Option<Throwable> specification = specification().f(lifecycle, contextInstance, specificationMethod); | |
final Option<NonEmptyList<Throwable>> result = preSpecificationSteps.nel().accumulate(throwables(), validate(specification).nel(), | |
validate(afters().f(lifecycle, contextInstance, contextClass.getAfterSpecificationMethods())).nel(), | |
validate(verifyMocks().f(lifecycle)).nel()); | |
if (result.isSome()) { | |
return fail(startTime, specificationMethod, result.some().toList(), specification); | |
} else { | |
return success(startTime, specificationMethod); | |
} | |
} | |
} | |
private F<SpecificationLifecycle, Option<Throwable>> resetMocks() { | |
return new F<SpecificationLifecycle, Option<Throwable>>() { | |
public Option<Throwable> f(final SpecificationLifecycle lifecycle) { | |
return lifecycle.resetMockery(); | |
} | |
}; | |
} | |
private F2<SpecificationLifecycle, Object, Either<Throwable, Unit>> wireActors() { | |
return new F2<SpecificationLifecycle, Object, Either<Throwable, Unit>>() { | |
public Either<Throwable, Unit> f(final SpecificationLifecycle lifecycle, final Object contextInstance) { | |
return rightToUnit(lifecycle.wireActors(contextInstance)); | |
} | |
}; | |
} | |
private F3<SpecificationLifecycle, Object, List<LifecycleMethod>, Option<Throwable>> befores() { | |
return new F3<SpecificationLifecycle, Object, List<LifecycleMethod>, Option<Throwable>>() { | |
public Option<Throwable> f(final SpecificationLifecycle lifecycle, final Object contextInstance, final List<LifecycleMethod> befores) { | |
return lifecycle.runBeforeSpecificationMethods(contextInstance, befores); | |
} | |
}; | |
} | |
private F3<SpecificationLifecycle, Object, SpecificationMethod, Option<Throwable>> specification() { | |
return new F3<SpecificationLifecycle, Object, SpecificationMethod, Option<Throwable>>() { | |
public Option<Throwable> f(final SpecificationLifecycle lifecycle, final Object contextInstance, final SpecificationMethod method) { | |
return lifecycle.runSpecification(contextInstance, method); | |
} | |
}; | |
} | |
private F3<SpecificationLifecycle, Object, List<LifecycleMethod>, Option<Throwable>> afters() { | |
return new F3<SpecificationLifecycle, Object, List<LifecycleMethod>, Option<Throwable>>() { | |
public Option<Throwable> f(final SpecificationLifecycle lifecycle, final Object contextInstance, final List<LifecycleMethod> afters) { | |
return lifecycle.runAfterSpecificationMethods(contextInstance, afters); | |
} | |
}; | |
} | |
private F<SpecificationLifecycle, Option<Throwable>> verifyMocks() { | |
return new F<SpecificationLifecycle, Option<Throwable>>() { | |
public Option<Throwable> f(final SpecificationLifecycle lifecycle) { | |
return lifecycle.verifyMocks(); | |
} | |
}; | |
} | |
@Suggest({"Push validation into the lifecycle"}) | |
private Validation<Throwable, Unit> validateSpecification(final ContextClass contextClass, final LifecycleMethod specificationMethod) { | |
final Validation<Throwable, Unit> contextValidation = validate(methodValidator.checkContextConstructor(contextClass.getType())); | |
final Validation<Throwable, Unit> beforesValidation = validate(validateMethods(contextClass.getBeforeSpecificationMethods())); | |
final Validation<Throwable, Unit> specValidation = validate(methodValidator.checkMethodHasNoParameters(specificationMethod)); | |
final Validation<Throwable, Unit> aftersValidation = validate(validateMethods(contextClass.getAfterSpecificationMethods())); | |
return contextValidation.sequence(beforesValidation).sequence(specValidation).sequence(aftersValidation); | |
} | |
private Option<Throwable> validateMethods(final List<LifecycleMethod> methods) { | |
final List<Option<Throwable>> results = methods.map(new F<LifecycleMethod, Option<Throwable>>() { | |
public Option<Throwable> f(final LifecycleMethod method) { | |
return methodValidator.checkMethodHasNoParameters(method); | |
} | |
}); | |
final List<Throwable> errors = somes(results); | |
final String message = "At least one specification lifecycle method failed validation"; | |
return errors.isEmpty() ? Option.<Throwable>none() : some((Throwable) new AggregatingException(message, errors)); | |
} | |
private SpecificationResult success(final long startTime, final LifecycleMethod specificationMethod) { | |
return new SpecificationResultImpl(specificationMethod.getName(), SPECIFICATION_SUCCESS, clock.getElapsedTime(startTime)); | |
} | |
private SpecificationResult fail(final long startTime, final LifecycleMethod specificationMethod, final Throwable error, | |
final Option<Throwable> specificationError) { | |
return fail(startTime, specificationMethod, List.<Throwable>nil().cons(error), specificationError); | |
} | |
private SpecificationResult fail(final long startTime, final LifecycleMethod specificationMethod, final List<Throwable> lifecycleErrors, | |
final Option<Throwable> specificationError) { | |
final List<Throwable> realErrors = realErrors(lifecycleErrors.reverse()); | |
final Option<Throwable> realSpecificationError = specificationError.map(new F<Throwable, Throwable>() { | |
public Throwable f(final Throwable throwable) { | |
return realError(throwable); | |
} | |
}); | |
final String message = "The following errors ocurred while running the specification"; | |
final SpecificationFailureException exception = new SpecificationFailureException(message, new AggregatingException(message, realErrors)); | |
final SpecificationRunStatus status = new SpecificationRunFailureStatus(exception, realSpecificationError); | |
return new SpecificationResultImpl(specificationMethod.getName(), status, clock.getElapsedTime(startTime)); | |
} | |
private Throwable realError(final Throwable lifecycleError) { | |
return realErrors(List.<Throwable>nil().cons(lifecycleError)).head(); | |
} | |
private List<Throwable> realErrors(final List<Throwable> lifecycleErrors) { | |
return lifecycleErrors.bind(new F<Throwable, List<Throwable>>() { | |
public List<Throwable> f(final Throwable throwable) { | |
if (throwable instanceof AggregatingException) { | |
return realErrors(((AggregatingException) throwable).getAggregatedErrors()); | |
} else { | |
return List.<Throwable>nil().cons(exceptionFinder.getRootCause(throwable)); | |
} | |
} | |
}); | |
} | |
@Suggest({"Push the obtaining of the lifecycle into the specification builder", "Groups that don't run get no-op lifecycle"}) | |
private SpecificationLifecycle lifecycle(final AnnotatedElement contextClass) { | |
final Class<? extends SpecificationLifecycle> lifecycleClass = | |
contextClass.isAnnotationPresent(Context.class) ? contextClass.getAnnotation(Context.class).lifecycle() | |
: StandardSpecificationLifecycle.class; | |
return constructorInvoker.invokeNullaryConstructor(lifecycleClass); | |
} | |
private Validation<Throwable, Unit> validate(final Option<Throwable> option) { | |
return validation(option.toEither(unit()).swap()); | |
} | |
private Either<Throwable, Unit> rightToUnit(final Either<Throwable, ?> either) { | |
return either.isLeft() ? Either.<Throwable, Unit>left(either.left().value()) : Either.<Throwable, Unit>right(unit()); | |
} | |
private Semigroup<NonEmptyList<Throwable>> throwables() { | |
return Semigroup.nonEmptyListSemigroup(); | |
} | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
private SpecificationResult runLifecycle(final long startTime, final SpecificationLifecycle lifecycle, | |
final SpecificationMethod specificationMethod) { | |
List<Option<Throwable>> lifecycleStepErrors = nil(); | |
final Either<Throwable, ContextClass> createContextResult = lifecycle.createContext(specificationMethod.getContextClass()); | |
lifecycleStepErrors = lifecycleStepErrors.cons(createContextResult.left().toOption()); | |
if (createContextResult.isLeft()) { | |
return fail(startTime, specificationMethod, somes(lifecycleStepErrors), false); | |
} else { | |
final ContextClass contextClass = createContextResult.right().value(); | |
final Option<Throwable> contextValidationResult = methodValidator.checkContextConstructor(contextClass.getType()); | |
lifecycleStepErrors = lifecycleStepErrors.cons(contextValidationResult); | |
if (contextValidationResult.isSome()) { | |
return fail(startTime, specificationMethod, somes(lifecycleStepErrors), false); | |
} else { | |
final Object contextInstance = constructorInvoker.invokeNullaryConstructor(contextClass.getType()); | |
lifecycleStepErrors = lifecycleStepErrors.cons(lifecycle.resetMockery()); | |
final Either<Throwable, List<Field>> wireResults = lifecycle.wireActors(contextInstance); | |
lifecycleStepErrors = lifecycleStepErrors.cons(wireResults.left().toOption()); | |
if (wireResults.isLeft()) { | |
return fail(startTime, specificationMethod, somes(lifecycleStepErrors), false); | |
} else { | |
final List<LifecycleMethod> befores = contextClass.getBeforeSpecificationMethods(); | |
final Option<Throwable> beforeValidationResults = validateMethods(befores); | |
lifecycleStepErrors = lifecycleStepErrors.cons(beforeValidationResults); | |
if (beforeValidationResults.isSome()) { | |
return fail(startTime, specificationMethod, somes(lifecycleStepErrors), false); | |
} else { | |
final Option<Throwable> beforeResult = lifecycle.runBeforeSpecificationMethods(contextInstance, befores); | |
lifecycleStepErrors = lifecycleStepErrors.cons(beforeResult); | |
if (beforeResult.isSome()) { | |
return fail(startTime, specificationMethod, somes(lifecycleStepErrors), false); | |
} else { | |
final Option<Throwable> specificationValidationResult = methodValidator.checkMethodHasNoParameters(specificationMethod); | |
lifecycleStepErrors = lifecycleStepErrors.cons(specificationValidationResult); | |
if (specificationValidationResult.isSome()) { | |
return fail(startTime, specificationMethod, somes(lifecycleStepErrors), false); | |
} else { | |
final Option<Throwable> specificationResult = lifecycle.runSpecification(contextInstance, specificationMethod); | |
lifecycleStepErrors = lifecycleStepErrors.cons(specificationResult); | |
lifecycleStepErrors = lifecycleStepErrors | |
.cons(lifecycle.runAfterSpecificationMethods(contextInstance, contextClass.getAfterSpecificationMethods())); | |
lifecycleStepErrors = lifecycleStepErrors.cons(lifecycle.verifyMocks()); | |
return determineResult(startTime, specificationMethod, specificationResult, lifecycleStepErrors); | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment