From 5c29bfc2c2bfe313e7bcbdf2cecfb31aa64395ed Mon Sep 17 00:00:00 2001 From: Mark Harrah Date: Fri, 4 Dec 2009 21:26:31 -0500 Subject: [PATCH] Getting scripted testing working with using-xsbt: * uses new scripted test framework in xsbt * adds ability to provide commands over loopback interface with state.destroy() + } + def send(message: String) = server.connection { _.send(message) } + def receive(errorMessage: String) = + server.connection { ipc => + val resultMessage = ipc.receive + if(!resultMessage.toBoolean) throw new TestFailed(errorMessage) + } + def newRemote = + { + val builder = new java.lang.ProcessBuilder("xsbt", "<" + server.port).directory(directory) + val io = BasicIO(log, false).withInput(_.close()) + val p = Process(builder) run( io ) + Spawn { p.exitValue(); server.close() } + try { receive("Remote sbt initialization failed") } + catch { case e: java.net.SocketException => error("Remote sbt initialization failed") } + p + } +// Process("java" :: "-classpath" :: classpath.map(_.getAbsolutePath).mkString(File.pathSeparator) :: "xsbt.boot.Boot" :: ( "<" + server.port) :: Nil) run log +} diff --git a/scripted/src/main/scala/Scripted.scala b/scripted/src/main/scala/Scripted.scala index 71f1a1a4c..9344bc2ee 100644 --- a/scripted/src/main/scala/Scripted.scala +++ b/scripted/src/main/scala/Scripted.scala @@ -4,7 +4,7 @@ package sbt.test import Scripted._ -import FileUtilities.{classLocation, sbtJar, scalaCompilerJar, scalaLibraryJar, wrapNull} +import FileUtilities.{sbtJar, scalaCompilerJar, scalaLibraryJar, wrapNull} import java.io.File import java.net.URLClassLoader @@ -17,18 +17,10 @@ trait ScalaScripted extends BasicScalaProject with Scripted with MavenStyleScala override def testAction = testNoScripted dependsOn(scripted) lazy val scriptedOnly = scriptedMethodTask(scriptedDependencies : _*) - - override def scriptedClasspath = runClasspath +++ Path.lazyPathFinder { Path.fromFile(sbtJar) :: Nil } } trait SbtScripted extends ScalaScripted { - override def scriptedDependencies = testCompile :: `package` :: Nil - override def scriptedClasspath = - Path.lazyPathFinder { - val ivy = runClasspath.get.filter(_.asFile.getName.startsWith("ivy-")).toList - val builtSbtJar = (outputPath / defaultJarName) - builtSbtJar :: ivy - } + override def scriptedDependencies = publishLocal :: Nil } final case class ScriptedTest(group: String, name: String) extends NotNull { @@ -36,6 +28,11 @@ final case class ScriptedTest(group: String, name: String) extends NotNull } trait Scripted extends Project with MultiTaskProject { + def scriptedCompatibility = CompatibilityLevel.Minimal + def scriptedDefScala = scalaVersion.value.toString + def scriptedSbt = projectVersion.value.toString + def scriptedBufferLog = true + def sbtTests: Path def scriptedTask(dependencies: ManagedTask*) = dynamic(scriptedTests(listTests)) dependsOn(dependencies : _*) def scriptedMethodTask(dependencies: ManagedTask*) = multiTask(listTests.map(_.toString).toList) { includeFunction => @@ -44,41 +41,21 @@ trait Scripted extends Project with MultiTaskProject def listTests = (new ListTests(sbtTests.asFile, include _, log)).listTests def scriptedTests(tests: Seq[ScriptedTest], dependencies: ManagedTask*) = { - val localLogger = new LocalLogger(log) - lazy val runner = - { - // load ScriptedTests using a ClassLoader that loads from the project classpath so that the version - // of sbt being built is tested, not the one doing the building. - val filtered = new FilteredLoader(ClassLoader.getSystemClassLoader, Seq("sbt.", "scala.", "ch.epfl.", "org.apache.", "org.jsch.")) - val loader = new URLClassLoader(_scriptedClasspath.toArray, filtered) - val scriptedClass = Class.forName(ScriptedClassName, true, loader) - val scriptedConstructor = scriptedClass.getConstructor(classOf[File], classOf[ClassLoader]) - val rawRunner = scriptedConstructor.newInstance(sbtTests.asFile, loader) - rawRunner.asInstanceOf[{def scriptedTest(group: String, name: String, log: Reflected.Logger): String}] - } + val runner = new ScriptedTests(sbtTests.asFile, scriptedBufferLog, scriptedSbt, scriptedDefScala, scriptedCompatibility) val startTask = task { None } named("scripted-test-start") dependsOn(dependencies : _*) def scriptedTest(test: ScriptedTest) = - task { unwrapOption(runner.scriptedTest(test.group, test.name, localLogger)) } named test.toString dependsOn(startTask) + task { runner.scriptedTest(test.group, test.name, log) } named test.toString dependsOn(startTask) val testTasks = tests.map(scriptedTest) task { None } named("scripted-test-complete") dependsOn(testTasks : _*) } private def unwrapOption[T](s: T): Option[T] = if(s == null) None else Some(s) - /** The classpath to use for scripted tests. This ensures that the version of sbt being built is the one used for testing.*/ - private def _scriptedClasspath = - { - val buildClasspath = classLocation[Scripted] - val scalaJars = List(scalaLibraryJar, scalaCompilerJar).map(_.toURI.toURL).toList - buildClasspath :: scalaJars ::: scriptedClasspath.get.map(_.asURL).toList - } - def scriptedClasspath: PathFinder = Path.emptyPathFinder def include(test: ScriptedTest) = true } import scala.collection.mutable private[test] object Scripted { - val ScriptedClassName = "sbt.test.ScriptedTests" val SbtTestDirectoryName = "sbt-test" def list(directory: File, filter: java.io.FileFilter) = wrapNull(directory.listFiles(filter)) } @@ -87,7 +64,6 @@ private[test] final class ListTests(baseDirectory: File, accept: ScriptedTest => def filter = DirectoryFilter -- HiddenFileFilter def listTests: Seq[ScriptedTest] = { - System.setProperty("sbt.scala.version", "") list(baseDirectory, filter) flatMap { group => val groupName = group.getName listTests(group).map(ScriptedTest(groupName, _)) diff --git a/scripted/src/main/scala/ScriptedLogger.scala b/scripted/src/main/scala/ScriptedLogger.scala deleted file mode 100644 index e7be8b30a..000000000 --- a/scripted/src/main/scala/ScriptedLogger.scala +++ /dev/null @@ -1,50 +0,0 @@ -package sbt.test - -object Reflected -{ - type Logger = - { - def enableTrace(flag: Boolean): Unit - def traceEnabled: Boolean - def getLevel: Int - def setLevel(level: Int): Unit - - def trace(t: F0[Throwable]): Unit - def success(message: F0[String]): Unit - def log(level: Int, message: F0[String]): Unit - def control(event: Int, message: F0[String]): Unit - } - type F0[T] = - { - def apply(): T - } -} - -final class LocalLogger(logger: Logger) extends NotNull -{ - import Reflected.F0 - def enableTrace(flag: Boolean) = logger.enableTrace(flag) - def traceEnabled = logger.traceEnabled - def getLevel = logger.getLevel.id - def setLevel(level: Int) = logger.setLevel(Level(level)) - - def trace(t: F0[Throwable]) = logger.trace(t()) - def success(message: F0[String]) = logger.success(message()) - def log(level: Int, message: F0[String]) = logger.log(Level(level), message()) - def control(event: Int, message: F0[String]) = logger.control(ControlEvent(event), message()) -} - -final class RemoteLogger(logger: Reflected.Logger) extends Logger -{ - private final class F0[T](s: => T) extends NotNull { def apply(): T = s } - def getLevel: Level.Value = Level(logger.getLevel) - def setLevel(newLevel: Level.Value) = logger.setLevel(newLevel.id) - def enableTrace(flag: Boolean) = logger.enableTrace(flag) - def traceEnabled = logger.traceEnabled - - def trace(t: => Throwable) = logger.trace(new F0(t)) - def success(message: => String) = logger.success(new F0(message)) - def log(level: Level.Value, message: => String) = logger.log(level.id, new F0(message)) - def control(event: ControlEvent.Value, message: => String) = logger.control(event.id, new F0(message)) - def logAll(events: Seq[LogEvent]) = events.foreach(log) -} \ No newline at end of file diff --git a/scripted/src/main/scala/ScriptedTests.scala b/scripted/src/main/scala/ScriptedTests.scala index b785618bb..8e7597873 100644 --- a/scripted/src/main/scala/ScriptedTests.scala +++ b/scripted/src/main/scala/ScriptedTests.scala @@ -5,59 +5,100 @@ package sbt.test import java.io.File +import java.nio.charset.Charset -final class ScriptedTests(testResources: Resources) extends NotNull +import xsbt.IPC +import xsbt.test.{CommentHandler, FileCommands, ScriptRunner, TestScriptParser} + +final class ScriptedTests(resourceBaseDirectory: File, bufferLog: Boolean, sbtVersion: String, defScalaVersion: String, level: CompatibilityLevel.Value) extends NotNull { - def this(resourceBaseDirectory: File, additional: ClassLoader) = this(new Resources(resourceBaseDirectory, additional)) - def this(resourceBaseDirectory: File) = this(new Resources(resourceBaseDirectory)) + private val testResources = new Resources(resourceBaseDirectory) val ScriptFilename = "test" - import testResources._ - private def printClass(c: Class[_]) = println(c.getName + " loader=" +c.getClassLoader + " location=" + FileUtilities.classLocationFile(c)) - - def scriptedTest(group: String, name: String, logger: Reflected.Logger): String = - { - val log = new RemoteLogger(logger) - val result = readOnlyResourceDirectory(group, name).fold(err => Some(err), testDirectory => scriptedTest(testDirectory, log)) - translateOption(result) - } - private def scriptedTest(testDirectory: File, log: Logger): Option[String] = + def scriptedTest(group: String, name: String, log: Logger): Option[String] = + testResources.readWriteResourceDirectory(group, name, log) { testDirectory => + scriptedTest(group + " / " + name, testDirectory, log).toLeft(()) + }.left.toOption + private def scriptedTest(label: String, testDirectory: File, log: Logger): Option[String] = + IPC.pullServer( scriptedTest0(label, testDirectory, log) ) + private def scriptedTest0(label: String, testDirectory: File, log: Logger)(server: IPC.Server): Option[String] = { + FillProperties(testDirectory, sbtVersion, defScalaVersion, level) val buffered = new BufferedLogger(log) - //buffered.startRecording() - val filtered = new FilterLogger(buffered) - val parsedScript = (new TestScriptParser(testDirectory, filtered)).parse(new File(testDirectory, ScriptFilename)) - val result = parsedScript.right.flatMap(withProject(testDirectory, filtered)) - //result.left.foreach(x => buffered.playAll()) - //buffered.clearAll() - result.left.toOption + if(bufferLog) + buffered.recordAll + + def createParser() = + { + val fileHandler = new FileCommands(testDirectory) + val sbtHandler = new SbtHandler(testDirectory, buffered, server) + new TestScriptParser(Map('$' -> fileHandler, '>' -> sbtHandler, '#' -> CommentHandler)) + } + def runTest() = + { + val run = new ScriptRunner + val parser = createParser() + run(parser.parse(new File(testDirectory, ScriptFilename))) + } + + try + { + runTest() + buffered.info("+ " + label) + None + } + catch + { + case e: xsbt.test.TestException => + buffered.playAll() + buffered.error("x " + label) + if(e.getCause eq null) + buffered.error(" " + e.getMessage) + else + e.printStackTrace + Some(e.toString) + case e: Exception => + buffered.playAll() + buffered.error("x " + label) + throw e + } + finally { buffered.clearAll() } } - private[this] def translateOption[T >: Null](s: Option[T]): T = s match { case Some(t) => t; case None => null } } -// TODO: remove for sbt 0.5.3 -final class FilterLogger(delegate: Logger) extends BasicLogger +object CompatibilityLevel extends Enumeration { - def trace(t: => Throwable) - { - if(traceEnabled) - delegate.trace(t) - } - def log(level: Level.Value, message: => String) - { - if(atLevel(level)) - delegate.log(level, message) - } - def success(message: => String) - { - if(atLevel(Level.Info)) - delegate.success(message) - } - def control(event: ControlEvent.Value, message: => String) - { - if(atLevel(Level.Info)) - delegate.control(event, message) - } - def logAll(events: Seq[LogEvent]): Unit = events.foreach(delegate.log) + val Full, Basic, Minimal, Minimal27, Minimal28 = Value } +object FillProperties +{ + def apply(projectDirectory: File, sbtVersion: String, defScalaVersion: String, level: CompatibilityLevel.Value): Unit = + { + import xsbt.Paths._ + fill(projectDirectory / "project" / "build.properties", sbtVersion, defScalaVersion, getVersions(level)) + } + def fill(properties: File, sbtVersion: String, defScalaVersion: String, buildScalaVersions: String) + { + val toAppend = extraProperties(sbtVersion, defScalaVersion, buildScalaVersions) + xsbt.OpenResource.fileWriter(Charset.forName("ISO-8859-1"), true)(properties) { _.write(toAppend) } + } + def getVersions(level: CompatibilityLevel.Value) = + { + import CompatibilityLevel._ + level match + { + case Full => "2.7.2 2.7.3 2.7.5 2.7.7 2.8.0.Beta1-RC2 2.8.0-SNAPSHOT" + case Basic => "2.7.7 2.7.2 2.8.0.Beta1-RC2" + case Minimal => "2.7.7 2.8.0.Beta1-RC2" + case Minimal27 => "2.7.7" + case Minimal28 => "2.8.0.Beta1-RC2" + } + } + def extraProperties(sbtVersion: String, defScalaVersion: String, buildScalaVersions: String) = + +sbt.version={sbtVersion} +def.scala.version={defScalaVersion} +build.scala.versions={buildScalaVersions} +.text +} \ No newline at end of file diff --git a/scripted/src/main/scala/TestScriptParser.scala b/scripted/src/main/scala/TestScriptParser.scala deleted file mode 100644 index 84b8746c3..000000000 --- a/scripted/src/main/scala/TestScriptParser.scala +++ /dev/null @@ -1,264 +0,0 @@ -/* sbt -- Simple Build Tool - * Copyright 2009 Mark Harrah - */ - -package sbt.test - -import java.io.{BufferedReader, File, InputStreamReader} - -/* -statement* -statement ::= ('$' | '>') word+ '[' word ']' -word ::= [^ \[\]]+ -comment ::= '#' [^ \n\r]* ('\n' | '\r' | eof) -*/ -import scala.util.parsing.combinator._ -import scala.util.parsing.input.Positional - -import TestScriptParser._ -private class TestScriptParser(baseDirectory: File, log: Logger) extends RegexParsers with NotNull -{ - type Statement = Project => Either[String, ReloadProject] - type PStatement = Statement with Positional - - private def evaluateList(list: List[PStatement])(p: Project): WithProjectResult[Unit] = - list match - { - case Nil => ValueResult(()) - case head :: tail => - head(p) match - { - case Left(msg) => new ErrorResult(msg) - case Right(reload) => ContinueResult(p =>evaluateList(tail)(p), reload) - } - } - - def script: Parser[Project => WithProjectResult[Unit]] = rep1(space ~> statement <~ space) ^^ evaluateList - def statement: Parser[PStatement] = - positioned - { - (StartRegex ~! rep1(word) ~! "[" ~! word ~! "]") ^^ - { - case start ~ command ~ open ~ result ~ close => - val successExpected = result.toLowerCase == SuccessLiteral.toLowerCase - new Statement with Positional - { selfPositional => - def apply(p: Project) = - { - val result = - try - { - start match - { - case CommandStart => evaluateCommand(command, successExpected, selfPositional)(p) - case ActionStart => evaluateAction(command, successExpected)(p).toLeft(NoReload) - } - } - catch - { - case e: Exception => - log.trace(e) - Left(e.toString) - } - result.left.map(message => linePrefix(this) + message) - } - } - } - } - private def linePrefix(p: Positional) = "{line " + p.pos.line + "} " - def space = """(\s+|(\#[^\n\r]*))*""".r - def word: Parser[String] = ("\'" ~> "[^'\n\r]*".r <~ "\'") | ("\"" ~> "[^\"\n\r]*".r <~ "\"") | WordRegex - def parse(scriptFile: File): Either[String, Project => WithProjectResult[Unit]] = - { - def parseReader(reader: java.io.Reader) = - parseAll(script, reader) match - { - case Success(result, next) => Right(result) - case err: NoSuccess => - { - val pos = err.next.pos - Left("Could not parse test script '" + scriptFile.getCanonicalPath + - "' (" + pos.line + "," + pos.column + "): " + err.msg) - } - } - FileUtilities.readValue(scriptFile, log)(parseReader) - } - - private def scriptError(message: String): Some[String] = Some("Test script error: " + message) - private def wrongArguments(commandName: String, args: List[String]): Some[String] = - scriptError("Command '" + commandName + "' does not accept arguments (found '" + spacedString(args) + "').") - private def wrongArguments(commandName: String, requiredArgs: String, args: List[String]): Some[String] = - scriptError("Wrong number of arguments to " + commandName + " command. " + requiredArgs + " required, found: '" + spacedString(args) + "'.") - private def evaluateCommand(command: List[String], successExpected: Boolean, position: Positional)(project: Project): Either[String, ReloadProject] = - { - command match - { - case "reload" :: Nil => Right(if(successExpected) new ReloadSuccessExpected(linePrefix(position)) else ReloadErrorExpected) - case x => evaluateCommandNoReload(x, successExpected)(project).toLeft(NoReload) - } - } - private def evaluateCommandNoReload(command: List[String], successExpected: Boolean)(project: Project): Option[String] = - { - evaluate(successExpected, "Command '" + command.firstOption.getOrElse("") + "'", project) - { - command match - { - case Nil => scriptError("No command specified.") - case "touch" :: paths => touch(paths, project) - case "delete" :: paths => delete(paths, project) - case "mkdir" :: paths => makeDirectories(paths, project) - case "copy-file" :: from :: to :: Nil => copyFile(from, to, project) - case "copy-file" :: args => wrongArguments("copy-file", "Two paths", args) - case "sync" :: from :: to :: Nil => sync(from, to, project) - case "sync" :: args => wrongArguments("sync", "Two directory paths", args) - case "copy" :: paths => copy(paths, project) - case "exists" :: paths => exists(paths, project) - case "absent" :: paths => absent(paths, project) - case "pause" :: Nil => readLine("Press enter to continue. "); println(); None - case "pause" :: args => wrongArguments("pause", args) - case "newer" :: a :: b :: Nil => newer(a, b, project) - case "newer" :: args => wrongArguments("newer", "Two paths", args) - case "sleep" :: time :: Nil => trap("Error while sleeping:") { Thread.sleep(time.toLong) } - case "sleep" :: args => wrongArguments("sleep", "Time in milliseconds", args) - case "exec" :: command :: args => execute(command, args, project) - case "exec" :: other => wrongArguments("exec", "Command and arguments", other) - case "reload" :: args => wrongArguments("reload", args) - case unknown :: arguments => scriptError("Unknown command " + unknown) - } - } - } - private def foreachBufferedLogger(project: Project)(f: BufferedLogger => Unit) - { - project.topologicalSort.foreach(p => p.log match { case buffered: BufferedLogger => f(buffered); case _ => () }) - } - private def evaluate(successExpected: Boolean, label: String, project: Project)(body: => Option[String]): Option[String] = - { - def startRecordingLog() { foreachBufferedLogger(project)(_.startRecording()) } - def clearLog() { foreachBufferedLogger(project)(_.clearAll()) } - def playLog(message: String) = - { - foreachBufferedLogger(project)(_.playAll()) - Some(message) - } - - startRecordingLog() - try - { - val result = body - if(result.isEmpty == successExpected) - None - else - { - val mainMessage = result.map("failed (expected success): " + _).getOrElse("succeeded (expected failure).") - playLog(label + " " + mainMessage) - } - } - finally { clearLog() } - } - private def evaluateAction(action: List[String], successExpected: Boolean)(project: Project): Option[String] = - { - def actionToString = action.mkString(" ") - action match - { - case Nil => scriptError("No action specified.") - case head :: Nil if project.taskNames.toSeq.contains(head)=> - evaluate(successExpected, "Action '" + actionToString + "'", project)(project.act(head)) - case head :: tail => - evaluate(successExpected, "Method '" + actionToString + "'", project)(project.call(head, tail.toArray)) - } - } - private def spacedString[T](l: Seq[T]) = l.mkString(" ") - private def wrap(result: Option[String]) = result.flatMap(scriptError) - private def trap(errorPrefix: String)(action: => Unit) = wrap( Control.trapUnit(errorPrefix, log) { action; None } ) - - private def fromStrings(paths: List[String], project: Project) = paths.map(path => fromString(path, project)) - private def fromString(path: String, project: Project) = Path.fromString(project.info.projectPath, path) - private def touch(paths: List[String], project: Project) = - if(paths.isEmpty) - scriptError("No paths specified for touch command.") - else - wrap(lazyFold(paths) { path => FileUtilities.touch(fromString(path, project), log) }) - - private def delete(paths: List[String], project: Project) = - if(paths.isEmpty) - scriptError("No paths specified for delete command.") - else - wrap(FileUtilities.clean(fromStrings(paths, project), true, log)) - private def sync(from: String, to: String, project: Project) = - wrap(FileUtilities.sync(fromString(from, project), fromString(to, project), log)) - private def copyFile(from: String, to: String, project: Project) = - wrap(FileUtilities.copyFile(fromString(from, project), fromString(to, project), log)) - private def copy(paths: List[String], project: Project) = - paths match - { - case Nil => scriptError("No paths specified for copy command.") - case path :: Nil => scriptError("No destination specified for copy command.") - case _ => - val mapped = fromStrings(paths, project).toArray - val last = mapped.length - 1 - wrap(FileUtilities.copy(mapped.take(last), mapped(last), log).left.toOption) - } - private def makeDirectories(paths: List[String], project: Project) = - fromStrings(paths, project) match - { - case Nil => scriptError("No paths specified for mkdir command.") - case p => FileUtilities.createDirectories(p, project.log) - } - private def newer(a: String, b: String, project: Project) = - trap("Error testing if '" + a + "' is newer than '" + b + "'") - { - val pathA = fromString(a, project) - val pathB = fromString(b, project) - pathA.exists && (!pathB.exists || pathA.lastModified > pathB.lastModified) - } - private def exists(paths: List[String], project: Project) = - fromStrings(paths, project).filter(!_.exists) match - { - case Nil => None - case x => Some("File(s) did not exist: " + x.mkString("[ ", " , ", " ]")) - } - private def absent(paths: List[String], project: Project) = - fromStrings(paths, project).filter(_.exists) match - { - case Nil => None - case x => Some("File(s) existed: " + x.mkString("[ ", " , ", " ]")) - } - private def execute(command: String, args: List[String], project: Project) = - { - if(command.trim.isEmpty) - Some("Command was empty.") - else - { - Control.trapUnit("Error running command: ", project.log) - { - val builder = new java.lang.ProcessBuilder((command :: args).toArray : _*).directory(project.info.projectDirectory) - val exitValue = Process(builder) ! log - if(exitValue == 0) - None - else - Some("Nonzero exit value (" + exitValue + ")") - } - } - } -} -private object TestScriptParser -{ - val SuccessLiteral = "success" - val Failure = "error" - val CommandStart = "$" - val ActionStart = ">" - val WordRegex = """[^ \[\]\s'\"][^ \[\]\s]*""".r - val StartRegex = ("[" + CommandStart + ActionStart + "]").r - - final def lazyFold[T](list: List[T])(f: T => Option[String]): Option[String] = - list match - { - case Nil => None - case head :: tail => - f(head) match - { - case None => lazyFold(tail)(f) - case x => x - } - } -} diff --git a/src/main/scala/sbt/Main.scala b/src/main/scala/sbt/Main.scala index cbdc9eca1..c75357577 100755 --- a/src/main/scala/sbt/Main.scala +++ b/src/main/scala/sbt/Main.scala @@ -116,46 +116,115 @@ class xMain extends xsbti.AppMain case r => r } } + /** This is the top-level command processing method. */ private def processArguments(baseProject: Project, arguments: List[String], configuration: xsbti.AppConfiguration, startTime: Long): xsbti.MainResult = { - def process(project: Project, arguments: List[String], isInteractive: Boolean): xsbti.MainResult = + type OnFailure = Option[String] + def ExitOnFailure = None + lazy val interactiveContinue = Some( InteractiveCommand ) + def remoteContinue(port: Int) = Some( FileCommandsPrefix + "-" + port ) + + // replace in 2.8 + trait Trampoline + class Done(val r: xsbti.MainResult) extends Trampoline + class Continue(project: Project, arguments: List[String], failAction: OnFailure) extends Trampoline { + def apply() = process(project, arguments, failAction) + } + def continue(project: Project, arguments: List[String], failAction: OnFailure) = new Continue(project, arguments, failAction) + def result(r: xsbti.MainResult) = new Done(r) + def run(t: Trampoline): xsbti.MainResult = t match { case d: Done => d.r; case c: Continue => run(c()) } + + def process(project: Project, arguments: List[String], failAction: OnFailure): Trampoline = { - def rememberCurrent(newArgs: List[String]) = if(baseProject.name != project.name) (ProjectAction + " " + project.name) :: newArgs else newArgs + project.log.debug("commands " + failAction.map("(on failure: " + _ + "): ").mkString + arguments.mkString(", ")) + def rememberCurrent(newArgs: List[String]) = rememberProject(rememberFail(newArgs)) + def rememberProject(newArgs: List[String]) = if(baseProject.name != project.name) (ProjectAction + " " + project.name) :: newArgs else newArgs + def rememberFail(newArgs: List[String]) = failAction.map(f => (FailureHandlerPrefix + f)).toList ::: newArgs + def failed(code: Int) = + failAction match + { + case Some(c) => continue(project, c :: Nil, ExitOnFailure) + case None => result( Exit(code) ) + } + arguments match { - case "" :: tail => process(project, tail, isInteractive) - case (ExitCommand | QuitCommand) :: _ => Exit(NormalExitCode) - case RebootCommand :: tail => Reboot(project.defScalaVersion.value, rememberCurrent(tail), configuration) - case InteractiveCommand :: _ => process(project, prompt(baseProject, project) :: arguments, true) + case "" :: tail => continue(project, tail, failAction) + case (ExitCommand | QuitCommand) :: _ => result( Exit(NormalExitCode) ) + case RebootCommand :: tail => result( Reboot(project.defScalaVersion.value, rememberCurrent(tail), configuration) ) + case InteractiveCommand :: _ => continue(project, prompt(baseProject, project) :: arguments, interactiveContinue) case SpecificBuild(version, action) :: tail => if(Some(version) != baseProject.info.buildScalaVersion) throw new ReloadException(rememberCurrent(action :: tail), Some(version)) else - process(project, action :: tail, isInteractive) + continue(project, action :: tail, failAction) + case CrossBuild(action) :: tail => - if(checkAction(project, action)) process(project, CrossBuild(project, action) ::: tail, isInteractive) - else if(isInteractive) process(project, tail, isInteractive) - else Exit(UsageErrorExitCode) + if(checkAction(project, action)) + process(project, CrossBuild(project, action) ::: tail, failAction) + else + failed(UsageErrorExitCode) + case SetProject(name) :: tail => SetProject(baseProject, name, project) match { - case Some(newProject) => process(newProject, tail, isInteractive) - case None => process(project, if(isInteractive) tail else ExitCommand :: tail, isInteractive) + case Some(newProject) => continue(newProject, tail, failAction) + case None => failed(BuildErrorExitCode) } + case action :: tail if action.startsWith(FileCommandsPrefix) => - val file = new File(baseProject.info.projectDirectory, action.substring(FileCommandsPrefix.length).trim) - readLines(project, file) match + getSource(action.substring(FileCommandsPrefix.length).trim, baseProject.info.projectDirectory) match { - case Some(lines) => process(project, lines ::: tail , isInteractive) - case None => process(project, if(isInteractive) tail else ExitCommand :: tail, isInteractive) + case Left(portAndSuccess) => + val port = Math.abs(portAndSuccess) + val previousSuccess = portAndSuccess >= 0 + readMessage(port, previousSuccess) match + { + case Some(message) => continue(project, message :: (FileCommandsPrefix + port) :: Nil, remoteContinue(port)) + case None => + project.log.error("Connection closed.") + failed(BuildErrorExitCode) + } + + case Right(file) => + readLines(project, file) match + { + case Some(lines) => continue(project, lines ::: tail , failAction) + case None => failed(UsageErrorExitCode) + } } + + case action :: tail if action.startsWith(FailureHandlerPrefix) => + val errorAction = action.substring(FailureHandlerPrefix.length).trim + continue(project, tail, if(errorAction.isEmpty) None else Some(errorAction) ) + case action :: tail => - val success = processAction(baseProject, project, action, isInteractive) - if(success || isInteractive) process(project, tail, isInteractive) else Exit(BuildErrorExitCode) - case Nil => project.log.error("Invalid internal sbt state: no arguments"); Exit(ProgramErrorExitCode) + val success = processAction(baseProject, project, action, failAction == interactiveContinue) + if(success) continue(project, tail, failAction) + else failed(BuildErrorExitCode) + + case Nil => + project.log.error("Invalid internal sbt state: no arguments") + result( Exit(ProgramErrorExitCode) ) } } - process(baseProject, arguments, arguments.lastOption == Some(InteractiveCommand)) + run(process(baseProject, arguments, ExitOnFailure)) + } + private def isInteractive(failureActions: Option[List[String]]) = failureActions == Some(InteractiveCommand :: Nil) + private def getSource(action: String, baseDirectory: File) = + { + try { Left(action.toInt) } + catch { case _: NumberFormatException => Right(new File(baseDirectory, action)) } + } + private def readMessage(port: Int, previousSuccess: Boolean): Option[String] = + { + // split into two connections because this first connection ends the previous communication + xsbt.IPC.client(port) { _.send(previousSuccess.toString) } + // and this second connection starts the next communication + xsbt.IPC.client(port) { ipc => + val message = ipc.receive + if(message eq null) None else Some(message) + } } object SetProject { @@ -261,8 +330,10 @@ class xMain extends xsbti.AppMain * Scala version between this prefix and the space (i.e. '++version action' means execute 'action' using * Scala version 'version'. */ val SpecificBuildPrefix = "++" - /** The prefix used to identify a file to read commands from. */ + /** The prefix used to identify a file or local port to read commands from. */ val FileCommandsPrefix = "<" + /** The prefix used to identify the action to run after an error*/ + val FailureHandlerPrefix = "!" /** The number of seconds between polling by the continuous compile command.*/ val ContinuousCompilePollDelaySeconds = 1 diff --git a/src/sbt-test/actions/interactive/test b/src/sbt-test/actions/interactive/test index 58946fe31..5493ef914 100644 --- a/src/sbt-test/actions/interactive/test +++ b/src/sbt-test/actions/interactive/test @@ -1,46 +1,46 @@ # This test verifies the behavior of actions declared interactive # Single project, non-interactive task -> interactive-test [success] -$ exists ran [success] -$ delete ran [success] +> interactive-test +$ exists ran +$ delete ran # Single project, interactive task -$ copy-file changes/TestProject2.scala project/build/src/TestProject.scala [success] -$ reload [success] -> interactive-test [success] -$ exists ran [success] -$ delete ran [success] +$ copy-file changes/TestProject2.scala project/build/src/TestProject.scala +> reload +> interactive-test +$ exists ran +$ delete ran # Multi-project, single interactive task on parent project -$ copy-file changes/TestProject3.scala project/build/src/TestProject.scala [success] -$ reload [success] -> interactive-test [success] -$ exists ran [success] -$ delete ran [success] +$ copy-file changes/TestProject3.scala project/build/src/TestProject.scala +> reload +> interactive-test +$ exists ran +$ delete ran # Multi-project, single interactive task on child project -$ copy-file changes/TestProject4.scala project/build/src/TestProject.scala [success] -$ reload [success] -> interactive-test [failure] +$ copy-file changes/TestProject4.scala project/build/src/TestProject.scala +> reload +-> interactive-test # Multi-project, two interactive tasks with same name, which is allowed because it is defined on parent -$ copy-file changes/TestProject5.scala project/build/src/TestProject.scala [success] -$ reload [success] -> interactive-test [success] -$ exists "ran" [success] -$ delete "ran" [success] +$ copy-file changes/TestProject5.scala project/build/src/TestProject.scala +> reload +> interactive-test +$ exists "ran" +$ delete "ran" # Multi-project, interactive on subproject + non-interactive on parent, which cannot be run from parent -$ copy-file changes/TestProject6.scala project/build/src/TestProject.scala [success] -$ reload [success] -> interactive-test [failure] +$ copy-file changes/TestProject6.scala project/build/src/TestProject.scala +> reload +-> interactive-test # Multi-project, two non-interactive tasks with same name, which is allowed -$ copy-file changes/TestProject7.scala project/build/src/TestProject.scala [success] -$ reload [success] -> interactive-test [success] -$ exists "ran" [success] -$ exists "a/ran" [success] -$ delete "ran" [success] -$ delete "a/ran" [success] \ No newline at end of file +$ copy-file changes/TestProject7.scala project/build/src/TestProject.scala +> reload +> interactive-test +$ exists "ran" +$ exists "a/ran" +$ delete "ran" +$ delete "a/ran" \ No newline at end of file diff --git a/src/sbt-test/compiler-project/run-test/project/build/src/TestProject.scala b/src/sbt-test/compiler-project/run-test/project/build/src/TestProject.scala index f9ab1439c..e08ad8e61 100644 --- a/src/sbt-test/compiler-project/run-test/project/build/src/TestProject.scala +++ b/src/sbt-test/compiler-project/run-test/project/build/src/TestProject.scala @@ -3,5 +3,4 @@ import sbt._ class TestProject(info: ProjectInfo) extends DefaultProject(info) { override def unmanagedClasspath = super.unmanagedClasspath +++ Path.fromFile(FileUtilities.sbtJar) - val sc = "org.scala-tools.testing" % "scalacheck" % "1.5" % "test->default" } diff --git a/src/sbt-test/compiler-project/run-test/src/main/scala/Foo.scala b/src/sbt-test/compiler-project/run-test/src/main/scala/Foo.scala index 1b58be453..b81426eb7 100644 --- a/src/sbt-test/compiler-project/run-test/src/main/scala/Foo.scala +++ b/src/sbt-test/compiler-project/run-test/src/main/scala/Foo.scala @@ -1,12 +1,15 @@ package foo.bar +import java.io.File +import java.net.{URISyntaxException, URL} + class Holder { var value: Any = _ } import scala.tools.nsc.{Interpreter, Settings} class Foo { val settings = new Settings() - settings.classpath.value = sbt.FileUtilities.classLocationFile[Holder].getAbsolutePath + settings.classpath.value = location(classOf[Holder]) val inter = new Interpreter(settings) def eval(code: String): Any = { @@ -15,6 +18,10 @@ class Foo { val r = inter.interpret("$r_.value = " + code) h.value } + def location(c: Class[_]) = toFile(c.getProtectionDomain.getCodeSource.getLocation).getAbsolutePath + def toFile(url: URL) = + try { new File(url.toURI) } + catch { case _: URISyntaxException => new File(url.getPath) } } object Test @@ -22,6 +29,6 @@ object Test def main(args: Array[String]) { val foo = new Foo - foo.eval("3") + args.foreach { arg => foo.eval(arg) == arg.toInt } } } diff --git a/src/sbt-test/compiler-project/run-test/src/test/scala/FooTest.scala b/src/sbt-test/compiler-project/run-test/src/test/scala/FooTest.scala deleted file mode 100644 index fb0601e2d..000000000 --- a/src/sbt-test/compiler-project/run-test/src/test/scala/FooTest.scala +++ /dev/null @@ -1,12 +0,0 @@ -package foo.bar - -import org.scalacheck._ - -object FooTest extends Properties("Foo") -{ - specify("Set", (i: Int) => { try { - val foo = new Foo - foo.eval(i.toString) == i - } catch { case e => e.printStackTrace(); false } - }) -} \ No newline at end of file diff --git a/src/sbt-test/compiler-project/run-test/test b/src/sbt-test/compiler-project/run-test/test index dd9d7d54a..15139f18e 100644 --- a/src/sbt-test/compiler-project/run-test/test +++ b/src/sbt-test/compiler-project/run-test/test @@ -1,11 +1,8 @@ -> update -[success] +> +run 1 +> +clean -> run -[success] +> +run 2 +> +clean -> clean -[success] - -> test -[success] +> +run -1 +> +clean \ No newline at end of file diff --git a/src/sbt-test/java/analysis/test b/src/sbt-test/java/analysis/test index 0e5cfa345..3641069b7 100644 --- a/src/sbt-test/java/analysis/test +++ b/src/sbt-test/java/analysis/test @@ -1,2 +1 @@ -> compile -[success] \ No newline at end of file +> +compile \ No newline at end of file diff --git a/src/sbt-test/java/basic/project/build.properties b/src/sbt-test/java/basic/project/build.properties index 3a234a13f..43f4b082f 100644 --- a/src/sbt-test/java/basic/project/build.properties +++ b/src/sbt-test/java/basic/project/build.properties @@ -1,5 +1,3 @@ -#Project properties -#Sat Apr 18 15:22:08 EDT 2009 project.organization=empty project.name=Java Test -project.version=1.0 +project.version=1.0 \ No newline at end of file diff --git a/src/sbt-test/java/basic/test b/src/sbt-test/java/basic/test index 0e5cfa345..3641069b7 100644 --- a/src/sbt-test/java/basic/test +++ b/src/sbt-test/java/basic/test @@ -1,2 +1 @@ -> compile -[success] \ No newline at end of file +> +compile \ No newline at end of file diff --git a/src/sbt-test/java/options/project/build/src/JavaProject.scala b/src/sbt-test/java/options/project/build/src/JavaProject.scala index 99bdd529e..5dbd40e0a 100644 --- a/src/sbt-test/java/options/project/build/src/JavaProject.scala +++ b/src/sbt-test/java/options/project/build/src/JavaProject.scala @@ -5,4 +5,5 @@ class JavaProject(info: ProjectInfo) extends DefaultProject(info) { // make the source target 1.4 so that we get an error when these options are used override def javaCompileOptions = ("-source" :: "1.4" :: Nil).map(JavaCompileOption(_)) + println(FileUtilities.classLocationFile[JavaProject]) } \ No newline at end of file diff --git a/src/sbt-test/java/options/test b/src/sbt-test/java/options/test index a10e99135..aa3abd024 100644 --- a/src/sbt-test/java/options/test +++ b/src/sbt-test/java/options/test @@ -1,2 +1,7 @@ -> compile -[failure] \ No newline at end of file +# need explicit versions here because we want: +# fail forall versions +# and not +# fail for any version +-> ++2.7.7 compile +-> ++2.8.0-SNAPSHOT compile +-> ++2.7.2 compile \ No newline at end of file diff --git a/src/sbt-test/java/separate/test b/src/sbt-test/java/separate/test index fc6cce7bf..1796e8a3d 100644 --- a/src/sbt-test/java/separate/test +++ b/src/sbt-test/java/separate/test @@ -1,9 +1,4 @@ > clean -[success] - > test -[success] - # this will fail if the sources are recompiled again (see the project definition) -> test -[success] \ No newline at end of file +> test \ No newline at end of file diff --git a/src/sbt-test/java/track-anonymous/project/build/AnonTest.scala b/src/sbt-test/java/track-anonymous/project/build/AnonTest.scala index 4119dca17..bc8b8342a 100644 --- a/src/sbt-test/java/track-anonymous/project/build/AnonTest.scala +++ b/src/sbt-test/java/track-anonymous/project/build/AnonTest.scala @@ -2,4 +2,12 @@ import sbt._ class AnonTest(info: ProjectInfo) extends DefaultProject(info) { override def compileOrder = CompileOrder.JavaThenScala + lazy val checkOutput = task { args => println(args.mkString); checkOutputTask(args(0) == "exists") } + private def checkOutputTask(shouldExist: Boolean) = + task + { + if((mainCompilePath / "Anon.class").exists != shouldExist) Some("Top level class incorrect" ) + else if( (mainCompilePath / "Anon$1.class").exists != shouldExist) Some("Inner class incorrect" ) + else None + } } \ No newline at end of file diff --git a/src/sbt-test/java/track-anonymous/test b/src/sbt-test/java/track-anonymous/test index 75a470c0e..ebdf4ee0a 100644 --- a/src/sbt-test/java/track-anonymous/test +++ b/src/sbt-test/java/track-anonymous/test @@ -1,17 +1,6 @@ $ copy-file changes/Anon.java src/main/java/Anon.java -[success] - -> compile -[success] - -$ exists target/classes/Anon.class target/classes/Anon$1.class -[success] - +> +compile +> +check-output exists $ delete src/main/java/Anon.java -[success] - -> compile -[success] - -$ absent target/classes/Anon.class target/classes/Anon$1.class -[success] \ No newline at end of file +> +compile +> +check-output absent \ No newline at end of file