diff --git a/sbt/src/sbt-test/project/scripted-bad-def/bad.sbt.disabled b/sbt/src/sbt-test/project/scripted-bad-def/bad.sbt.disabled new file mode 100644 index 000000000..a423e7372 --- /dev/null +++ b/sbt/src/sbt-test/project/scripted-bad-def/bad.sbt.disabled @@ -0,0 +1 @@ +notValid diff --git a/sbt/src/sbt-test/project/scripted-bad-def/test b/sbt/src/sbt-test/project/scripted-bad-def/test new file mode 100644 index 000000000..3845dd842 --- /dev/null +++ b/sbt/src/sbt-test/project/scripted-bad-def/test @@ -0,0 +1,13 @@ +# a test to verify that a bad build definition doesn't hang scripted tests +# and that scripted can recover when reload is expected to fail + +$ copy-file bad.sbt.disabled bad.sbt +-> reload + +# removing the problematic build should allow the next command to continue +$ delete bad.sbt + +# due to the previous reload failing, this will force the project to be reloaded first +# and this should then succeed +> help + diff --git a/scripted/sbt/src/main/scala/sbt/test/SbtHandler.scala b/scripted/sbt/src/main/scala/sbt/test/SbtHandler.scala index 3e27d721c..4956b1b9f 100644 --- a/scripted/sbt/src/main/scala/sbt/test/SbtHandler.scala +++ b/scripted/sbt/src/main/scala/sbt/test/SbtHandler.scala @@ -10,31 +10,56 @@ package test import Logger._ -final class SbtHandler(directory: File, launcher: File, log: Logger, server: IPC.Server, launchOpts: Seq[String] = Seq()) extends StatementHandler +final case class SbtInstance(process: Process, server: IPC.Server) + +final class SbtHandler(directory: File, launcher: File, log: Logger, launchOpts: Seq[String] = Seq()) extends StatementHandler { - def this(directory: File, launcher: File, log: xsbti.Logger, server: IPC.Server, launchOpts: Seq[String]) = this(directory, launcher, log: Logger, server, launchOpts) - type State = Process - def initialState = newRemote - def apply(command: String, arguments: List[String], p: Process): Process = - { - send((command :: arguments.map(escape)).mkString(" ")) - receive(command + " failed") - p + type State = Option[SbtInstance] + def initialState = None + + def apply(command: String, arguments: List[String], i: Option[SbtInstance]): Option[SbtInstance] = onSbtInstance(i) { (process, server) => + send((command :: arguments.map(escape)).mkString(" "), server) + receive(command + " failed", server) } - def finish(state: Process) = - try { - server.connection { _.send("exit") } - state.exitValue() - } catch { - case e: IOException => state.destroy() + def onSbtInstance(i: Option[SbtInstance])(f: (Process, IPC.Server) => Unit): Option[SbtInstance] = i match { + case Some(ai @ SbtInstance(process, server)) if server.isClosed => + finish(i) + onNewSbtInstance(f) + case Some(SbtInstance(process, server)) => + f(process, server) + i + case None => + onNewSbtInstance(f) + } + private[this] def onNewSbtInstance(f: (Process, IPC.Server) => Unit): Option[SbtInstance] = + { + val server = IPC.unmanagedServer + val p = try newRemote(server) catch { case e: Throwable => server.close(); throw e } + val ai = Some(SbtInstance(p, server)) + try f(p, server) catch { case e: Throwable => + // TODO: closing is necessary only because StatementHandler uses exceptions for signaling errors + finish(ai); throw e } - def send(message: String) = server.connection { _.send(message) } - def receive(errorMessage: String) = + ai + } + + def finish(state: Option[SbtInstance]) = state match { + case Some(SbtInstance(process, server)) => + try { + send("exit", server) + process.exitValue() + } catch { + case e: IOException => process.destroy() + } + case None => + } + def send(message: String, server: IPC.Server) = server.connection { _.send(message) } + def receive(errorMessage: String, server: IPC.Server) = server.connection { ipc => val resultMessage = ipc.receive if(!resultMessage.toBoolean) throw new TestFailed(errorMessage) } - def newRemote = + def newRemote(server: IPC.Server): Process = { val launcherJar = launcher.getAbsolutePath val globalBase = "-Dsbt.global.base=" + (new File(directory, "global")).getAbsolutePath @@ -42,8 +67,8 @@ final class SbtHandler(directory: File, launcher: File, log: Logger, server: IPC val io = BasicIO(log, false).withInput(_.close()) val p = Process(args, directory) run( io ) Spawn { p.exitValue(); server.close() } - try { receive("Remote sbt initialization failed") } - catch { case e: java.net.SocketException => error("Remote sbt initialization failed") } + try { receive("Remote sbt initialization failed", server) } + catch { case e: java.net.SocketException => throw new TestFailed("Remote sbt initialization failed") } p } import java.util.regex.Pattern.{quote => q} diff --git a/scripted/sbt/src/main/scala/sbt/test/ScriptedTests.scala b/scripted/sbt/src/main/scala/sbt/test/ScriptedTests.scala index d227275f5..891bd0f8b 100644 --- a/scripted/sbt/src/main/scala/sbt/test/ScriptedTests.scala +++ b/scripted/sbt/src/main/scala/sbt/test/ScriptedTests.scala @@ -45,8 +45,6 @@ final class ScriptedTests(resourceBaseDirectory: File, bufferLog: Boolean, launc } } private def scriptedTest(label: String, testDirectory: File, log: Logger): Unit = - IPC.pullServer( scriptedTest0(label, testDirectory, log) ) - private def scriptedTest0(label: String, testDirectory: File, log: Logger)(server: IPC.Server) { val buffered = new BufferedLogger(new FullLogger(log)) if(bufferLog) @@ -55,7 +53,7 @@ final class ScriptedTests(resourceBaseDirectory: File, bufferLog: Boolean, launc def createParser() = { val fileHandler = new FileCommands(testDirectory) - val sbtHandler = new SbtHandler(testDirectory, launcher, buffered, server, launchOpts) + val sbtHandler = new SbtHandler(testDirectory, launcher, buffered, launchOpts) new TestScriptParser(Map('$' -> fileHandler, '>' -> sbtHandler, '#' -> CommentHandler)) } val (file, pending) = {