Skip to content

Commit

Permalink
collect NotImplementedErrors in AllExpectations. fixes #298
Browse files Browse the repository at this point in the history
Conflicts:
	common/src/main/scala/org/specs2/execute/ResultExecution.scala
	core/src/main/scala/org/specs2/specification/ResultsContext.scala
	core/src/main/scala/org/specs2/specification/process/Stats.scala
	core/src/test/scala/org/specs2/specification/AllExpectationsSpec.scala
	junit/src/main/scala/org/specs2/matcher/JUnitExpectations.scala
	junit/src/main/scala/org/specs2/reporter/JUnitReporter.scala
	matcher/src/main/scala/org/specs2/matcher/MatchResult.scala
	matcher/src/main/scala/org/specs2/matcher/Matcher.scala
	matcher/src/main/scala/org/specs2/matcher/StandardMatchResults.scala
  • Loading branch information
etorreborre committed Oct 1, 2014
1 parent ce5af0e commit 9a02e38
Show file tree
Hide file tree
Showing 16 changed files with 250 additions and 64 deletions.
54 changes: 33 additions & 21 deletions common/src/main/scala/org/specs2/execute/Result.scala
Expand Up @@ -133,6 +133,10 @@ sealed abstract class Result(val message: String = "", val expected: String = ""
* @return true if the result is a Failure instance
*/
def isFailure: Boolean = false
/**
* @return true if the result is a Failure that was thrown like a JUnit assertion error or a NotImplemented exception
*/
def isThrownFailure: Boolean = false
/**
* @return the result with no message
*/
Expand Down Expand Up @@ -161,7 +165,7 @@ object Result {

case (Success(msg1, e1), Failure(msg2, e2, st1, d2)) => m2.updateMessage(msg1+"; "+msg2)
case (Failure(msg1, e1, st1, d1), Failure(msg2, e2, st2, d2)) => Failure(msg1+"; "+msg2,
concat(e1, e2), st1, NoDetails())
concat(e1, e2), st1, NoDetails)

case (Success(msg1, e1), Error(msg2, st1)) => m2.updateMessage(msg1+"; "+msg2)
case (Error(msg1, st1), Error(msg2, st2)) => Error(msg1+"; "+msg2, st1)
Expand All @@ -188,25 +192,28 @@ object Result {
*/
implicit val ResultFailureMonoid: Monoid[Result] = ResultFailuresMonoid("; ")
implicit def ResultFailuresMonoid(separator: String): Monoid[Result] = new Monoid[Result] {
val zero = Success()
def append(m1: Result, m2: =>Result) = {
val zero = Success()

def append(m1: Result, m2: => Result) = {
(m1, m2) match {
case (Success(msg1, e1), Success(msg2, e2)) => Success("", concat(e1, e2))
case (Success(msg1, e1), other) => other
case (other, Success(msg2, e2)) => other
case (Failure(msg1, e1, st1, d1), Failure(msg2, e2, st2, d2)) => Failure(concat(msg1, msg2, separator), e1+separator+e2, st1, NoDetails())
case (Error(msg1, st1), Error(msg2, st2)) => Error(concat(msg1, msg2, separator), st1)
case (Error(msg1, st1), Failure(msg2, e2, st2, d2)) => Error(concat(msg1, msg2, separator), st1)
case (Skipped(msg1, e1), Skipped(msg2, e2)) => Skipped(concat(msg1, msg2, separator), e1+separator+e2)
case (Skipped(msg1, e1), Pending(msg2)) => Pending(concat(msg1, msg2, separator))
case (Pending(msg1), Skipped(msg2, e2)) => Pending(concat(msg1, msg2, separator))
case (Pending(msg1), Pending(msg2)) => Pending(concat(msg1, msg2, separator))
case (DecoratedResult(t, r), other) => DecoratedResult(t, append(r, other))
case (other, DecoratedResult(t, r)) => DecoratedResult(t, append(other, r))
case (Failure(msg1, e1, st, d), _) => m1
case (Error(msg1, st), _) => m1
case (_, Failure(msg1, e1, st, d)) => m2
case (_, Error(msg1, st)) => m2
case (Success(msg1, e1), Success(msg2, e2)) => Success("", concat(e1, e2))
case (Success(msg1, e1), other) => other
case (other, Success(msg2, e2)) => other
case (Failure(msg1, e1, st1, d1), Failure(msg2, e2, st2, NoDetails)) => Failure(concat(msg1, msg2, separator), e1 + separator + e2, st1, d1)
case (Failure(msg1, e1, st1, NoDetails), Failure(msg2, e2, st2, d2)) => Failure(concat(msg1, msg2, separator), e1 + separator + e2, st2, d2)
case (Failure(msg1, e1, st1, d1), Failure(msg2, e2, st2, d2)) => Failure(concat(msg1, msg2, separator), e1 + separator + e2, st1, d1)
case (Error(msg1, st1), Error(msg2, st2)) => Error(concat(msg1, msg2, separator), st1)
case (Error(msg1, st1), Failure(msg2, e2, st2, d2)) => Error(concat(msg1, msg2, separator), st1)
case (Skipped(msg1, e1), Skipped(msg2, e2)) => Skipped(concat(msg1, msg2, separator), e1 + separator + e2)
case (Skipped(msg1, e1), Pending(msg2)) => Pending(concat(msg1, msg2, separator))
case (Pending(msg1), Skipped(msg2, e2)) => Pending(concat(msg1, msg2, separator))
case (Pending(msg1), Pending(msg2)) => Pending(concat(msg1, msg2, separator))
case (DecoratedResult(t, r), other) => DecoratedResult(t, append(r, other))
case (other, DecoratedResult(t, r)) => DecoratedResult(t, append(other, r))
case (Failure(msg1, e1, st, d), _) => m1
case (Error(msg1, st), _) => m1
case (_, Failure(msg1, e1, st, d)) => m2
case (_, Error(msg1, st)) => m2
}
}.setExpectationsNb(m1.expectationsNb + m2.expectationsNb)
}
Expand Down Expand Up @@ -276,7 +283,7 @@ object Success {
* This class represents the failure of an execution.
* It has a message and a stacktrace
*/
case class Failure(m: String = "", e: String = "", stackTrace: List[StackTraceElement] = new Exception().getStackTrace.toList, details: Details = NoDetails())
case class Failure(m: String = "", e: String = "", stackTrace: List[StackTraceElement] = new Exception().getStackTrace.toList, details: Details = NoDetails)
extends Result(m, e) with ResultStackTrace { outer =>

type SelfType = Failure
Expand All @@ -298,6 +305,8 @@ case class Failure(m: String = "", e: String = "", stackTrace: List[StackTraceEl
}
override def hashCode = m.hashCode + e.hashCode
override def isFailure: Boolean = true
override def isThrownFailure: Boolean =
Seq(FromNotImplementedError, FromJUnitAssertionError).contains(details)

def skip: Skipped = Skipped(m, e)
}
Expand All @@ -307,7 +316,10 @@ case class Failure(m: String = "", e: String = "", stackTrace: List[StackTraceEl
*/
sealed trait Details
case class FailureDetails(expected: String, actual: String) extends Details
case class NoDetails() extends Details
case object NoDetails extends Details
case object FromNotImplementedError extends Details
case object FromJUnitAssertionError extends Details

/**
* This class represents an exception occurring during an execution.
*/
Expand Down
Expand Up @@ -27,8 +27,8 @@ trait ResultExecution { outer =>
case ErrorException(f) => f
case DecoratedResultException(f) => f
case e: Exception => Error(e)
case e: AssertionError if fromJUnit(e) => Failure(e.getMessage.notNull, "", e.getStackTrace.toList)
case e: java.lang.Error if simpleClassName(e) == "NotImplementedError" => Failure(e.getMessage.notNull, "", e.getStackTrace.toList)
case e: AssertionError if fromJUnit(e) => Failure(e.getMessage.notNull, "", e.getStackTrace.toList, details = FromNotImplementedError)
case e: java.lang.Error if simpleClassName(e) == "NotImplementedError" => Failure(e.getMessage.notNull, "", e.getStackTrace.toList, details = FromJUnitAssertionError)
case e: Throwable => Error(e)
}

Expand All @@ -53,8 +53,8 @@ trait ResultExecution { outer =>
case e: ErrorException => throw e
case e: DecoratedResultException => throw e
case e: Exception => throw ErrorException(Error(e))
case e: AssertionError if fromJUnit(e) => throw FailureException(Failure(e.getMessage.notNull, "", e.getStackTrace.toList))
case e: java.lang.Error if simpleClassName(e) == "NotImplementedError" => throw FailureException(Failure(e.getMessage.notNull, "", e.getStackTrace.toList))
case e: AssertionError if fromJUnit(e) => throw FailureException(Failure(e.getMessage.notNull, "", e.getStackTrace.toList, details = FromNotImplementedError))
case e: java.lang.Error if simpleClassName(e) == "NotImplementedError" => throw FailureException(Failure(e.getMessage.notNull, "", e.getStackTrace.toList, details = FromJUnitAssertionError))
case e: Throwable => throw ErrorException(Error(e))
}

Expand Down
9 changes: 5 additions & 4 deletions core/src/main/scala/org/specs2/specification/Context.scala
Expand Up @@ -185,10 +185,11 @@ trait StoredResultsContext extends Context { this: { def storedResults: Seq[Resu
def apply[T : AsResult](r: =>T): Result = {
// evaluate r, triggering side effects
val asResult = AsResult(r)

// if the execution returns an Error (and not match failures)
val results = storedResults
// if the execution returns an Error or a Failure that was created for a thrown
// exception, like a JUnit assertion error or a NotImplementedError
// then add the result as a new issue
if (asResult.isError) issues(storedResults :+ asResult, "\n")
else issues(storedResults, "\n")
if (asResult.isError || asResult.isThrownFailure) issues(results :+ asResult, "\n")
else issues(results, "\n")
}
}
41 changes: 41 additions & 0 deletions core/src/main/scala/org/specs2/specification/ResultsContext.scala
@@ -0,0 +1,41 @@
package org.specs2
package specification

import org.specs2.execute.{Success, ResultExecution, AsResult, Result}
import execute.Result._
import matcher.StoredExpectations

/**
* This class is used to evaluate a Context as a sequence of results by side-effects.
*
* @see the AllExpectations trait for its use
*/
class ResultsContext(results: =>Seq[Result]) extends StoredResultsContext {
def storedResults = results
}

/**
* This trait can be used when it is not desirable to use the AllExpectations trait, that is, when the specification
* examples must be executed concurrently and not isolated.
*
* @see the UserGuide on how to use this trait
*/
trait StoredExpectationsContext extends StoredExpectations with StoredResultsContext

/**
* This trait is a context which will use the results provided by the class inheriting that trait.
* It evaluates the result of an example, which is supposed to create side-effects
* and returns the 'storedResults' as the summary of all results
*/
trait StoredResultsContext extends Context { this: { def storedResults: Seq[Result]} =>
def apply[T : AsResult](r: =>T): Result = {
// evaluate r, triggering side effects
val asResult = AsResult(r)
val results = storedResults
// if the execution returns an Error or a Failure that was created for a thrown
// exception, like a JUnit assertion error or a NotImplementedError
// then add the result as a new issue
if (asResult.isError || asResult.isThrownFailure) issues(results :+ asResult, "\n")
else issues(results, "\n")
}
}
Expand Up @@ -3,6 +3,7 @@ package specification
package process

import control._
import org.specs2.data.Fold
import specification.core._
import scalaz.concurrent.Task
import scalaz.stream._
Expand Down Expand Up @@ -45,4 +46,21 @@ trait Statistics {

}

object Statistics extends Statistics
object Statistics extends Statistics {
/** compute the statistics as a Fold */
def statisticsFold = new Fold[Fragment] {
type S = Stats

def prepare: Task[Unit] = Task.now(())

lazy val sink: Sink[Task, (Fragment, Stats)] = Fold.unitSink

def fold = Statistics.fold
def init = Stats.empty

def last(stats: Stats) = Task.now(())
}

def runStats(spec: SpecStructure): Stats =
Fold.runFoldLast(spec.contents, Statistics.statisticsFold).run
}
@@ -0,0 +1,95 @@
package org.specs2
package specification

import _root_.org.specs2.mutable.{Specification => Spec}
import org.specs2.data.Fold
import org.specs2.execute.Result
import org.specs2.main.Arguments
import org.specs2.specification.core.{Env, ContextualSpecificationStructure, SpecificationStructure}
import org.specs2.specification.process.{DefaultSelector, Statistics, Stats, DefaultExecutor}
import reporter._
import user.specification._

class AllExpectationsSpec extends Spec with AllExpectations {

"A specification with the AllExpectations trait should" >> {
"evaluate all its expectations" >> {
executed.hasIssues must beTrue
executed.expectations === 10
executed.examples === 4
executed.failures === 4
executedIssues.head.message === "'1' is not equal to '2' [AllExpectationsSpecification.scala:8]\n"+
"'1' is not equal to '3' [AllExpectationsSpecification.scala:9]"
}
"short-circuit the rest of the example if an expectation fails and uses 'orThrow'" >> {
executedIssues.map(_.message).toList must containMatch("'51' is not equal to '52'")
executedIssues.map(_.message) must not containMatch("'13' is not equal to '14'")
}
"short-circuit the rest of the example if an expectation fails and uses 'orSkip'" >> {
executedSuspended.map(_.message).toList must not containMatch("'51' is not equal to '52'")
executedIssues.map(_.message) must not containMatch("'15' is not equal to '16'")
}
"work ok on a specification with selected fragments" >> {
executedSelected.hasIssues must beTrue
executedSelected.expectations === 4
executedSelected.failures === 2
}
"work ok on a sequential specification" >> {
executedSequential.hasIssues must beTrue
executedSequential.expectations === 10
executedSequential.failures === 4
}
"work ok on a mutable specification with Scopes" >> {
executedWithScope.hasIssues must beTrue
executedWithScope.expectations === 3
executedWithScope.failures === 1
executedWithScopeIssues.head.message === "'1' is not equal to '2' [AllExpectationsSpecification.scala:31]\n"+
"'1' is not equal to '3' [AllExpectationsSpecification.scala:32]"
}
"evaluate an exception" >> {
executedException.hasIssues must beTrue
executedException.expectations === 1
executedException.examples === 1
executedException.errors === 1
}
"evaluate an expression which is not implemented yet" >> {
executedWithNotImplementedError.hasIssues must beTrue
executedWithNotImplementedError.expectations === 2
executedWithNotImplementedError.examples === 1
executedWithNotImplementedError.errors === 0
executedWithNotImplementedError.failures === 1
}
}

def stats(spec: ContextualSpecificationStructure)(args: Arguments): Stats = {
val env = Env(arguments = args)
try {
val executed = DefaultExecutor.executeSpecWithoutShutdown(spec.structure(env) |> DefaultSelector.select(env), env)
Statistics.runStats(executed)
}
finally env.shutdown
}

def results(spec: ContextualSpecificationStructure)(args: Arguments): IndexedSeq[Result] = {
val env = Env(arguments = args)
try DefaultExecutor.executeSpecWithoutShutdown(spec.structure(env), env).fragments.fragments.map(_.executionResult)
finally env.shutdown
}

def issues(spec: ContextualSpecificationStructure)(args: Arguments): IndexedSeq[Result] =
results(spec)(args).filter(r => r.isError || r.isFailure)

def suspended(spec: ContextualSpecificationStructure)(args: Arguments): IndexedSeq[Result] =
results(spec)(args).filter(r => r.isSkipped || r.isPending)

def executed = stats(new AllExpectationsSpecification)(args())
def executedIssues = issues(new AllExpectationsSpecification)(args())
def executedSuspended = suspended(new AllExpectationsSpecification)(args())
def executedException = stats(new AllExpectationsSpecificationWithException)(args())
def executedSelected = stats(new AllExpectationsSpecification)(args(ex = "It is"))
def executedSequential = stats(new AllExpectationsSpecification)(sequential)
def executedWithScope = stats(new AllExpectationsSpecificationWithScope)(args())
def executedWithScopeIssues = issues(new AllExpectationsSpecificationWithScope)(args())
def executedWithNotImplementedError = stats(new AllExpectationsSpecificationWithNotImplementedError)(args())
}

Expand Up @@ -43,4 +43,11 @@ class AllExpectationsSpecificationWithException extends mutable.Specification wi
}
}


class AllExpectationsSpecificationWithNotImplementedError extends mutable.Specification with AllExpectations {
def notImplementedYet: Int = ???
"In this example the exception is caught" >> {
1 === 2
notImplementedYet must_== 1
3 === 4
}
}
Expand Up @@ -13,14 +13,22 @@ import execute._
trait JUnitExpectations extends ThrownExpectations {
override protected def checkFailure[T](m: MatchResult[T]) = {
m match {
<<<<<<< HEAD
case f @ MatchFailure(ok, ko, _, _, NoDetails()) => throw new AssertionFailedError(f.koMessage) {
=======
case f @ MatchFailure(ok, ko, _, FailureDetails(expected, actual)) => throw new ComparisonFailure(ko(), expected, actual) {
>>>>>>> d6fa545... collect NotImplementedErrors in AllExpectations. fixes #298
override def getStackTrace = f.exception.getStackTrace
override def getCause = f.exception.getCause
override def printStackTrace = f.exception.printStackTrace
override def printStackTrace(w: java.io.PrintStream) = f.exception.printStackTrace(w)
override def printStackTrace(w: java.io.PrintWriter) = f.exception.printStackTrace(w)
}
<<<<<<< HEAD
case f @ MatchFailure(ok, ko, _, _, FailureDetails(expected, actual)) => throw new ComparisonFailure(f.koMessage, expected, actual) {
=======
case f @ MatchFailure(ok, ko, _, _) => throw new AssertionFailedError(ko()) {
>>>>>>> d6fa545... collect NotImplementedErrors in AllExpectations. fixes #298
override def getStackTrace = f.exception.getStackTrace
override def getCause = f.exception.getCause
override def printStackTrace = f.exception.printStackTrace
Expand Down
4 changes: 2 additions & 2 deletions matcher/src/main/scala/org/specs2/matcher/Expectations.scala
Expand Up @@ -32,7 +32,7 @@ trait ExpectationsCreation {
/** this method can be overriden to intercept a MatchResult and change its message before it is thrown */
protected def mapMatchResult[T](m: MatchResult[T]): MatchResult[T] = m
/** this method can be overriden to throw exceptions when checking the result */
protected def checkResultFailure(r: Result): Result = r
protected def checkResultFailure(r: =>Result): Result = r
/** this method can be overriden to throw exceptions when checking the match result */
protected def checkMatchResultFailure[T](m: MatchResult[T]) = m
protected def checkMatchResultFailure[T](m: MatchResult[T]): MatchResult[T] = m
}
3 changes: 2 additions & 1 deletion matcher/src/main/scala/org/specs2/matcher/MatchResult.scala
Expand Up @@ -124,10 +124,11 @@ object MatchSuccess {
def apply[T](ok: =>String, ko: =>String, expectable: Expectable[T]) =
new MatchSuccess(() => ok, () => ko, expectable)
}

case class MatchFailure[T] private[specs2](ok: () => String, ko: () => String,
expectable: Expectable[T],
trace: List[StackTraceElement] = (new Exception).getStackTrace.toList,
details: Details = NoDetails()) extends MatchResult[T] {
details: Details = NoDetails) extends MatchResult[T] {
lazy val okMessage = ok()
lazy val koMessage = ko()

Expand Down

0 comments on commit 9a02e38

Please sign in to comment.