From d3ee1d5942aaa37026da1856a129d44f6cc813bb Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Sat, 9 Apr 2022 15:12:20 -0700 Subject: [PATCH 1/2] Move on-termination test --- sbt-app/src/sbt-test/watch/{on-termination => on-error}/build.sbt | 0 .../watch/{on-termination => on-error}/project/Build.scala | 0 sbt-app/src/sbt-test/watch/{on-termination => on-error}/test | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename sbt-app/src/sbt-test/watch/{on-termination => on-error}/build.sbt (100%) rename sbt-app/src/sbt-test/watch/{on-termination => on-error}/project/Build.scala (100%) rename sbt-app/src/sbt-test/watch/{on-termination => on-error}/test (100%) diff --git a/sbt-app/src/sbt-test/watch/on-termination/build.sbt b/sbt-app/src/sbt-test/watch/on-error/build.sbt similarity index 100% rename from sbt-app/src/sbt-test/watch/on-termination/build.sbt rename to sbt-app/src/sbt-test/watch/on-error/build.sbt diff --git a/sbt-app/src/sbt-test/watch/on-termination/project/Build.scala b/sbt-app/src/sbt-test/watch/on-error/project/Build.scala similarity index 100% rename from sbt-app/src/sbt-test/watch/on-termination/project/Build.scala rename to sbt-app/src/sbt-test/watch/on-error/project/Build.scala diff --git a/sbt-app/src/sbt-test/watch/on-termination/test b/sbt-app/src/sbt-test/watch/on-error/test similarity index 100% rename from sbt-app/src/sbt-test/watch/on-termination/test rename to sbt-app/src/sbt-test/watch/on-error/test From 379503a894abc60e05d73c412417778b6228ec6e Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Sat, 9 Apr 2022 15:05:54 -0700 Subject: [PATCH 2/2] Restore watchOnTermination At some point the watchOnTermination callback stopped working. I'm not exactly sure how or why that happened but it is fairly straightforward to restore. The one tricky thing was that the callback has the signature (Watch.Action, _, _, _) => State, which requires propagating the action to the failWatch command. The easiest way to do this was to add a mutable field to the ContinuousState. This is rather ugly and reflects some poor design choices but a more comprehensive refactor is out of the scope of this fix. This commit adds a scripted test that ensures that the callback is invoked both in the successful and unsuccessful watch cases. In each case the callback deletes a file and we ensure that the file is indeed absent after the watch exits. --- .../main/scala/sbt/internal/Continuous.scala | 68 +++++++++++++------ .../src/sbt-test/watch/on-termination/bar.txt | 0 .../sbt-test/watch/on-termination/build.sbt | 15 ++++ .../src/sbt-test/watch/on-termination/foo.txt | 0 .../src/sbt-test/watch/on-termination/test | 8 +++ 5 files changed, 70 insertions(+), 21 deletions(-) create mode 100644 sbt-app/src/sbt-test/watch/on-termination/bar.txt create mode 100644 sbt-app/src/sbt-test/watch/on-termination/build.sbt create mode 100644 sbt-app/src/sbt-test/watch/on-termination/foo.txt create mode 100644 sbt-app/src/sbt-test/watch/on-termination/test diff --git a/main/src/main/scala/sbt/internal/Continuous.scala b/main/src/main/scala/sbt/internal/Continuous.scala index a95e21ea1..ea1b5134f 100644 --- a/main/src/main/scala/sbt/internal/Continuous.scala +++ b/main/src/main/scala/sbt/internal/Continuous.scala @@ -1128,7 +1128,28 @@ private[sbt] object Continuous extends DeprecatedContinuous { val callbacks: Callbacks, val dynamicInputs: mutable.Set[DynamicInput], val pending: Boolean, + var failAction: Option[Watch.Action], ) { + def this( + count: Int, + commands: Seq[String], + beforeCommandImpl: (State, mutable.Set[DynamicInput]) => State, + afterCommand: State => State, + afterWatch: State => State, + callbacks: Callbacks, + dynamicInputs: mutable.Set[DynamicInput], + pending: Boolean, + ) = this( + count, + commands, + beforeCommandImpl, + afterCommand, + afterWatch, + callbacks, + dynamicInputs, + pending, + None + ) def beforeCommand(state: State): State = beforeCommandImpl(state, dynamicInputs) def incremented: ContinuousState = withCount(count + 1) def withPending(p: Boolean) = @@ -1323,7 +1344,8 @@ private[sbt] object ContinuousCommands { case Watch.Prompt => stop.map(_ :: s"$PromptChannel ${channel.name}" :: Nil mkString ";") case Watch.Run(commands) => stop.map(_ +: commands.map(_.commandLine).filter(_.nonEmpty) mkString "; ") - case Watch.HandleError(_) => + case a @ Watch.HandleError(_) => + cs.failAction = Some(a) stop.map(_ :: s"$failWatch ${channel.name}" :: Nil mkString "; ") case _ => stop } @@ -1353,27 +1375,31 @@ private[sbt] object ContinuousCommands { } cs.afterCommand(postState) } - private[sbt] val stopWatchCommand = watchCommand(stopWatch) { (channel, state) => - state.get(watchStates).flatMap(_.get(channel)) match { - case Some(cs) => - val afterWatchState = cs.afterWatch(state) - cs.callbacks.onExit() - StandardMain.exchange - .channelForName(channel) - .foreach { c => - c.terminal.setPrompt(Prompt.Pending) - c.unprompt(ConsoleUnpromptEvent(Some(CommandSource(channel)))) + private[this] val exitWatchShared = (error: Boolean) => + (channel: String, state: State) => + state.get(watchStates).flatMap(_.get(channel)) match { + case Some(cs) => + val afterWatchState = cs.afterWatch(state) + cs.callbacks.onExit() + StandardMain.exchange + .channelForName(channel) + .foreach { c => + c.terminal.setPrompt(Prompt.Pending) + c.unprompt(ConsoleUnpromptEvent(Some(CommandSource(channel)))) + } + val newState = afterWatchState.get(watchStates) match { + case None => afterWatchState + case Some(w) => afterWatchState.put(watchStates, w - channel) } - afterWatchState.get(watchStates) match { - case None => afterWatchState - case Some(w) => afterWatchState.put(watchStates, w - channel) - } - case _ => state - } - } - private[sbt] val failWatchCommand = watchCommand(failWatch) { (channel, state) => - state.fail - } + val commands = cs.commands.mkString("; ") + val count = cs.count + val action = cs.failAction.getOrElse(Watch.CancelWatch) + val st = cs.callbacks.onTermination(action, commands, count, newState) + if (error) st.fail else st + case _ => if (error) state.fail else state + } + private[sbt] val stopWatchCommand = watchCommand(stopWatch)(exitWatchShared(false)) + private[sbt] val failWatchCommand = watchCommand(failWatch)(exitWatchShared(true)) /* * Creates a FileTreeRepository where it is safe to call close without inadvertently cancelling * still active watches. diff --git a/sbt-app/src/sbt-test/watch/on-termination/bar.txt b/sbt-app/src/sbt-test/watch/on-termination/bar.txt new file mode 100644 index 000000000..e69de29bb diff --git a/sbt-app/src/sbt-test/watch/on-termination/build.sbt b/sbt-app/src/sbt-test/watch/on-termination/build.sbt new file mode 100644 index 000000000..798f18016 --- /dev/null +++ b/sbt-app/src/sbt-test/watch/on-termination/build.sbt @@ -0,0 +1,15 @@ +watchOnIteration := { (count, project, commands) => + Watch.CancelWatch +} +watchOnTermination := { (action, count, command, state) => + action match { + case Watch.CancelWatch => + java.nio.file.Files.delete(java.nio.file.Paths.get("foo.txt")) + case Watch.HandleError(e) => + if (e.getMessage == "fail") + java.nio.file.Files.delete(java.nio.file.Paths.get("bar.txt")) + else + throw new IllegalStateException("unexpected error") + } + state +} diff --git a/sbt-app/src/sbt-test/watch/on-termination/foo.txt b/sbt-app/src/sbt-test/watch/on-termination/foo.txt new file mode 100644 index 000000000..e69de29bb diff --git a/sbt-app/src/sbt-test/watch/on-termination/test b/sbt-app/src/sbt-test/watch/on-termination/test new file mode 100644 index 000000000..1634ee847 --- /dev/null +++ b/sbt-app/src/sbt-test/watch/on-termination/test @@ -0,0 +1,8 @@ +$ exists foo.txt +> ~compile +$ absent foo.txt +> set watchOnIteration := { (_, _, _) => new Watch.HandleError(new IllegalStateException("fail")) } +$ exists bar.txt +-> ~compile +$ absent bar.txt +