org.specs.specification.BaseSpecification.scala Maven / Gradle / Ivy
Show all versions of specs_2.8.0.Beta1-RC8
/**
* Copyright (c) 2007-2009 Eric Torreborre
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
* documentation files (the "Software"), to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of
* the Software. Neither the name of specs nor the names of its contributors may be used to endorse or promote
* products derived from this software without specific prior written permission.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
* TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
package org.specs.specification
import org.specs.matcher.MatcherUtils._
import org.specs.SpecUtils._
import scala.reflect.Manifest
import org.specs.execute._
import org.specs.util._
import org.specs.util.ExtendedString._
import org.specs.Specification
/**
* This class provides the base structure of a specification.
* A specification has a name, a description and is composed of:
* - sub specifications and/or
* - systems under specification (systems)
*
*
* In turn the systems can contain recursively contain examples.
*
* The description of a specification is its class name by default but which can be also overriden for better readibility.
* For example, it is possible to declare a Specification using a constructor taking the full name of the specification:
*
* class messagingSpec extends Specification("Specification for the messaging system")
*
*
*
* A BaseSpecification
implements several traits:
* - TreeNode: allowing the specification, its systems and examples to be considered as a generic tree of nodes
* - SpecificationSystems: trait holding the systems declaration and navigation functions
* - SpecificationExecutor: this enables the execution of examples in isolation, by executing them in a clone of the specification, so that any local variable used
* by the example is set to its initial value as if other examples never had modified it. This trait specialize the ExampleLifeCycle trait defining all steps of an example execution
* - ExampleExpectationsListener: this trait allows an expectation to be added to the current example being executed
* - Tagged: allow to tag the specification with some name so that accepted and rejected tags define what should be executed or not
* - HasResults: generic trait for anything declaring failures, successes, errors and skipped
* - LinkedSpecification: this allow the declaration of links between literal specifications
* - SpecificationConfiguration: this defines variables which affect the behaviour of the specification. For example if examples without expectations
* should be marked as pending
*
*/
class BaseSpecification extends TreeNode with SpecificationSystems with SpecificationExecutor with ExampleExpectationsListener with Tagged
with HasResults with LinkedSpecification with SpecificationConfiguration
with ComposedSpecifications with LazyParameters { outer =>
/** name of the specification */
var name = createDescription(getClass.getName)
/** description of the specification */
var description = createDescription(getClass.getName)
/**
* @return a description from the class name, taking the last name which doesn't contain a $ or a number.
* For example: com.pack1.MyClass$1$ will:
* - split on $ and reverse: [1, com.pack1.MyClass]
*
- drop the every element which is an integer -> [com.pack1.MyClass]
*
- take the first element: com.pack1.MyClass
*
- split on . and reverse: [MyClass, pack1, com]
*
- take the last element: MyClass
*/
private[specs] def createDescription(s: String) = s.
split("\\$").reverse.
dropWhile(isInteger(_))(0).
split("\\.").
reverse.toList(0)
/** specifications contained by the current specification. An empty list by default */
var subSpecifications: List[Specification] = List()
/** specification which includes this one */
var parentSpecification: Option[BaseSpecification] = None
/** set the parent specification of this one */
def setParent(s: BaseSpecification): this.type = { parentSpecification = Some(s); this }
/** @return all the parent specifications of this specification, starting with the most immediate parent */
def parentSpecifications: List[BaseSpecification] = {
parentSpecification.map(List(_)).getOrElse(Nil) ::: parentSpecification.map(_.parentSpecifications).getOrElse(Nil)
}
/** this declares that a specification is composed of other specifications */
def isSpecifiedBy(specifications: LazyParameter[Specification]*) = {
addToName(" is specified by")
include(specifications:_*)
}
/** alias for isSpecifiedBy */
def areSpecifiedBy(specifications: LazyParameter[Specification]*) = {
addToName(" are specified by")
include(specifications:_*)
}
private def addToName(s: String) {
this.description = this.name + s
}
/**
* include a list of specifications inside this one
*/
def include(specifications: LazyParameter[Specification]*) = {
val toInclude = specifications.toStream.filter((s: LazyParameter[Specification]) => !(s() eq this) && !s().contains(this)).
map { s => s().setParent(this); s() }
subSpecifications = subSpecifications ++ toInclude
}
/** @return recursively all the systems included in this specification */
def allSystems: List[Sus] = {
systems ::: subSpecifications.flatMap(_.allSystems)
}
/** @return recursively all the examples included in this specification */
def allExamples: List[Examples] = {
systems.flatMap(_.allExamples) ::: subSpecifications.flatMap(_.allExamples)
}
/** @return true if it contains the specification recursively */
def contains(s: Any): Boolean = {
subSpecifications.contains(s) || subSpecifications.exists(_.contains(s))
}
/** @return the example corresponding to a given Tree path, searching in the incl */
def getExample(path: TreePath): Option[Examples] = {
path match {
case TreePath(0 :: i :: rest) if systems.size > i => systems(i).getExample(TreePath(rest))
case _ => None
}
}
/**
* implicit definition allowing to declare a new example described by a string desc
* Usage: "return 0 when asked for (0+0)" in {...}
* Alternatively, it could be created with:
* forExample("return 0 when asked for (0+0)").in {...}
*/
implicit def specifyExample(desc: String): ExampleSpecification = {
val example = exampleContainer.createExample(desc)
if (sequential) example.setSequential()
new ExampleSpecification(example)
}
class ExampleSpecification(val example: Example) {
def in(expectations: =>Any) = example.in(expectations)
def in(e: =>Examples): Unit = example.in(e)
def >>(expectations: =>Any) = example.>>(expectations)
def >>(e: =>Examples) = example.>>(e)
}
def forExample(desc: String): Example = {
specifyExample(desc).example
}
/**
* Create an anonymous example, giving it a number depending on the existing created examples/
*/
def forExample: Example = {
forExample("example " + (exampleContainer.exampleList.size + 1))
}
/**
* Return the example being currently executed if any
*/
def lastExample: Option[Examples] = {
current match {
case Some(s: Sus) => None
case Some(e: Example) => Some(e)
case None => None
}
}
/**
* utility method to track the last example list being currently defined.
* It is either the current sus (one gets created with specify if there's not any) or
* the current example
*/
protected[specification] def exampleContainer: Examples = {
current.getOrElse {
setCurrent(Some(specify))
current.get
}
}
/** the beforeAllSystems function will be invoked before all systems */
var beforeSpec: Option[() => Any] = None
/** the afterAllSystems function will be invoked after all systems */
var afterSpec: Option[() => Any] = None
private var beforeSpecHasBeenExecuted = false
/**
* override the beforeExample method to execute actions before the
* first example of the first sus
*/
override def beforeExample(ex: Examples) = {
if (!executeOneExampleOnly && !beforeSpecHasBeenExecuted) {
beforeSpecHasBeenExecuted = true
beforeSpec.map(_.apply)
}
super.beforeExample(ex)
}
/**
* override the afterExample method to execute actions after the
* last example of the last sus
*/
override def afterExample(ex: Examples) = {
super.afterExample(ex)
if (!systems.isEmpty &&
systems.last.executed &&
!systems.last.exampleList.isEmpty &&
systems.last.exampleList.last == ex)
afterSpec.map(_.apply)
}
/**
* this variable commands if the specification has been instantiated to execute one example only,
* in order to execute it in isolation
*/
private[specification] var executeOneExampleOnly = false
/**
* Syntactic sugar for examples sharing between systems under test.
* Usage:
* "A stack below full capacity" should {
* behave like "A non-empty stack below full capacity"
* ...
*
* In this example we suppose that there is a system under specification with the same name previously defined.
* Otherwise, an Exception would be thrown, causing the specification failure at construction time.
*/
object behave {
def like(other: Sus): Example = {
val behaveLike: Example = forExample("behave like " + other.description.uncapitalize)
behaveLike.in {
other.examples.foreach { o =>
val e = behaveLike.createExample(o.description.toString)
e.execution = o.execution
e.execution.map(_.example = e)
e.parent = Some(behaveLike)
}
}
behaveLike
}
def like(susName: String): Example = outer.systems.find(_.description == susName) match {
case Some(sus) => this.like(sus)
case None => throw new Exception(q(susName) + " is not specified in " + outer.name +
outer.systems.map(_.description).mkString(" (available sus are: ", ", ", ")"))
}
}
/** @return the first level examples number (i.e. without subexamples) */
def firstLevelExamplesNb: Int = subSpecifications.foldLeft(0)(_+_.firstLevelExamplesNb) + systems.foldLeft(0)(_+_.examples.size)
/** @return the failures of each sus */
def failures: List[FailureException] = subSpecifications.flatMap(_.failures) ::: systems.flatMap(_.failures)
/** @return the skipped of each sus */
def skipped: List[SkippedException] = subSpecifications.flatMap{_.skipped} ::: systems.flatMap(_.skipped)
/** @return the errors of each sus */
def errors: List[Throwable] = subSpecifications.flatMap(_.errors) ::: systems.flatMap(_.errors)
/** @return all the examples with no errors, failures or skip messages */
def successes: List[Example] = subSpecifications.flatMap(_.successes) ::: systems.flatMap(_.successes)
/** @return all the examples */
def examples: List[Example] = subSpecifications.flatMap(_.examples) ::: systems.flatMap(_.examples)
/** @return the total number of expectations for each sus */
def expectationsNb: Int = subSpecifications.foldLeft(0)(_ + _.expectationsNb) + systems.foldLeft(0)(_ + _.expectationsNb)
/** @return true if there are failures or errors */
def isFailing: Boolean = !this.failures.isEmpty || !this.errors.isEmpty
/** reset in order to be able to run the examples again */
def resetForExecution: this.type = {
subSpecifications.foreach(_.resetForExecution)
systems.foreach(_.resetForExecution)
this
}
/** Declare the subspecifications and systems as components to be tagged when the specification is tagged */
override def taggedComponents: List[Tagged] = this.systems.toList ::: this.subSpecifications
/** @return the name of the specification */
override def toString = name
}
trait ComposedSpecifications extends LazyParameters { this: BaseSpecification =>
/**
* implicit definition allowing to declare a composition inside the current specification:
* "A complex specification".isSpecifiedBy(spec1, spec2)
* It changes the name of this specification with the parameter
*/
implicit def declare(newName: String): ComposedSpecification = {
name = newName
new ComposedSpecification(this)
}
class ComposedSpecification(s: BaseSpecification) {
def isSpecifiedBy(specifications: LazyParameter[Specification]*) = s.isSpecifiedBy(specifications:_*)
def areSpecifiedBy(specifications: LazyParameter[Specification]*) = s.areSpecifiedBy(specifications:_*)
def include(specifications: LazyParameter[Specification]*) = s.include(specifications:_*)
}
}