From 919d0ac63d83a24adc2c99087bf2fe5c231db638 Mon Sep 17 00:00:00 2001 From: Mark Harrah Date: Wed, 3 Jul 2013 17:15:26 -0400 Subject: [PATCH] Failed task execution should still preserve State changes. Fixes #804. Candidate for inclusion in 0.13.0 if there is another RC, otherwise scheduled for 0.13.1. --- .../command/src/main/scala/sbt/MainLoop.scala | 24 +++++------------ main/command/src/main/scala/sbt/State.scala | 26 ++++++++++++++++++ main/src/main/scala/sbt/Aggregation.scala | 9 ++++--- .../update-state-fail/project/Build.scala | 27 +++++++++++++++++++ .../sbt-test/actions/update-state-fail/test | 9 +++++++ 5 files changed, 74 insertions(+), 21 deletions(-) create mode 100644 sbt/src/sbt-test/actions/update-state-fail/project/Build.scala create mode 100644 sbt/src/sbt-test/actions/update-state-fail/test diff --git a/main/command/src/main/scala/sbt/MainLoop.scala b/main/command/src/main/scala/sbt/MainLoop.scala index ad9829ef6a..c909382c72 100644 --- a/main/command/src/main/scala/sbt/MainLoop.scala +++ b/main/command/src/main/scala/sbt/MainLoop.scala @@ -75,23 +75,11 @@ object MainLoop case Left(t) => handleException(t, state) } - import ExceptionCategory._ + @deprecated("Use State.handleError", "0.13.0") + def handleException(e: Throwable, s: State): State = s.handleError(e) - def handleException(e: Throwable, s: State): State = - handleException(e, s, s.log) - def handleException(t: Throwable, s: State, log: Logger): State = - { - ExceptionCategory(t) match { - case AlreadyHandled => () - case m: MessageOnly => log.error(m.message) - case f: Full => logFullException(f.exception, log) - } - s.fail - } - def logFullException(e: Throwable, log: Logger) - { - log.trace(e) - log.error(ErrorHandling reducedToString e) - log.error("Use 'last' for the full log.") - } + @deprecated("Use State.handleError", "0.13.0") + def handleException(t: Throwable, s: State, log: Logger): State = State.handleException(t, s, log) + + def logFullException(e: Throwable, log: Logger): Unit = State.logFullException(e, log) } \ No newline at end of file diff --git a/main/command/src/main/scala/sbt/State.scala b/main/command/src/main/scala/sbt/State.scala index ec607a6791..16dd8f535f 100644 --- a/main/command/src/main/scala/sbt/State.scala +++ b/main/command/src/main/scala/sbt/State.scala @@ -79,6 +79,12 @@ trait StateOps { /** Marks the currently executing command as failing. This triggers failure handling by the command processor. See also `State.onFailure`*/ def fail: State + /** Marks the currently executing command as failing due to the given exception. + * This displays the error appropriately and triggers failure handling by the command processor. + * Note that this does not throw an exception and returns normally. + * It is only once control is returned to the command processor that failure handling at the command level occurs. */ + def handleError(t: Throwable): State + /** Schedules `newCommands` to be run after any remaining commands. */ def ++ (newCommands: Seq[Command]): State /** Schedules `newCommand` to be run after any remaining commands. */ @@ -190,6 +196,7 @@ object State def has(key: AttributeKey[_]) = s.attributes contains key def remove(key: AttributeKey[_]) = s.copy(attributes = s.attributes remove key) def log = s.globalLogging.full + def handleError(t: Throwable): State = handleException(t, s, log) def fail = { val remaining = s.remainingCommands.dropWhile(_ != FailureWall) @@ -205,6 +212,7 @@ object State case None => noHandler } + def addExitHook(act: => Unit): State = s.copy(exitHooks = s.exitHooks + ExitHook(act)) def runExitHooks(): State = { @@ -221,4 +229,22 @@ object State def initializeClassLoaderCache = s.put(BasicKeys.classLoaderCache, newClassLoaderCache) private[this] def newClassLoaderCache = new classpath.ClassLoaderCache(s.configuration.provider.scalaProvider.launcher.topLoader) } + + import ExceptionCategory._ + + private[sbt] def handleException(t: Throwable, s: State, log: Logger): State = + { + ExceptionCategory(t) match { + case AlreadyHandled => () + case m: MessageOnly => log.error(m.message) + case f: Full => logFullException(f.exception, log) + } + s.fail + } + private[sbt] def logFullException(e: Throwable, log: Logger) + { + log.trace(e) + log.error(ErrorHandling reducedToString e) + log.error("Use 'last' for the full log.") + } } \ No newline at end of file diff --git a/main/src/main/scala/sbt/Aggregation.scala b/main/src/main/scala/sbt/Aggregation.scala index 1e11f833e1..c18f2aafbe 100644 --- a/main/src/main/scala/sbt/Aggregation.scala +++ b/main/src/main/scala/sbt/Aggregation.scala @@ -47,8 +47,8 @@ final object Aggregation val log = state.log val extracted = Project extract state val success = results match { case Value(_) => true; case Inc(_) => false } - try { EvaluateTask.onResult(results, log) { results => if(show.taskValues) printSettings(results, show.print) } } - finally { if(show.success) printSuccess(start, stop, extracted, success, log) } + results.toEither.right.foreach { r => if(show.taskValues) printSettings(r, show.print) } + if(show.success) printSuccess(start, stop, extracted, success, log) } def timedRun[T](s: State, ts: Values[Task[T]], extra: DummyTaskMap): Complete[T] = { @@ -73,7 +73,10 @@ final object Aggregation def runTasks[HL <: HList, T](s: State, structure: BuildStructure, ts: Values[Task[T]], extra: DummyTaskMap, show: ShowConfig)(implicit display: Show[ScopedKey[_]]): State = { val complete = timedRun[T](s, ts, extra) showRun(complete, show) - complete.state + complete.results match { + case Inc(i) => complete.state.handleError(i) + case Value(_) => complete.state + } } def printSuccess(start: Long, stop: Long, extracted: Extracted, success: Boolean, log: Logger) diff --git a/sbt/src/sbt-test/actions/update-state-fail/project/Build.scala b/sbt/src/sbt-test/actions/update-state-fail/project/Build.scala new file mode 100644 index 0000000000..8f8070d4c9 --- /dev/null +++ b/sbt/src/sbt-test/actions/update-state-fail/project/Build.scala @@ -0,0 +1,27 @@ +import sbt._ +import Keys._ + +object TestBuild extends Build +{ + lazy val akey = AttributeKey[Int]("TestKey") + lazy val t = TaskKey[String]("test-task") + lazy val check = InputKey[Unit]("check") + lazy val root = Project("root", file(".")).aggregate(a, b).settings( + check := checkState(checkParser.parsed, state.value) + ) + + lazy val a = Project("a", file("a")).settings(t := error("Failing")) + + lazy val b = Project("b", file("b")).settings(t <<= Def.task("").updateState(updater)) + + def checkState(runs: Int, s: State) { + val stored = s.get(akey).getOrElse(0) + assert(stored == runs, "Expected " + runs + ", got " + stored) + } + + def updater(s: State, a: AnyRef): State = s.update(akey)(_.getOrElse(0) + 1) + + import complete.DefaultParsers._ + + lazy val checkParser = token(Space ~> IntBasic) +} diff --git a/sbt/src/sbt-test/actions/update-state-fail/test b/sbt/src/sbt-test/actions/update-state-fail/test new file mode 100644 index 0000000000..a3f2508c4d --- /dev/null +++ b/sbt/src/sbt-test/actions/update-state-fail/test @@ -0,0 +1,9 @@ +> check 0 +-> test-task +> check 1 +-> test-task +> check 2 +> b/test-task +> check 3 +-> a/test-task +> check 3 \ No newline at end of file