Getting scripted testing working with using-xsbt:

* uses new scripted test framework in xsbt
 * adds ability to provide commands over loopback interface with <port command
 * only some tests have been updated
This commit is contained in:
Mark Harrah 2009-12-04 21:26:31 -05:00
parent dfaa2f370c
commit 5c29bfc2c2
24 changed files with 317 additions and 504 deletions

View File

@ -1,7 +1,5 @@
#Project properties
#Thu Nov 19 22:26:26 EST 2009
project.organization=org.scala-tools.sbt
project.name=Simple Build Tool
sbt.version=0.5.6
project.version=0.6.4
project.version=0.6.5-p1
scala.version=2.7.5

View File

@ -5,7 +5,7 @@ import sbt._
import java.io.File
class SbtProject(info: ProjectInfo) extends DefaultProject(info) //with test.SbtScripted
class SbtProject(info: ProjectInfo) extends DefaultProject(info) with test.SbtScripted
{
/* Additional resources to include in the produced jar.*/
def extraResources = descendents(info.projectPath / "licenses", "*") +++ "LICENSE" +++ "NOTICE"
@ -13,12 +13,18 @@ class SbtProject(info: ProjectInfo) extends DefaultProject(info) //with test.Sbt
override def testOptions = ExcludeTests("sbt.ReflectiveSpecification" :: Nil) :: super.testOptions.toList
override def normalizedName = "sbt"
//override def scriptedDependencies = testCompile :: `package` :: Nil
override def managedStyle = ManagedStyle.Ivy
val publishTo = Resolver.file("test-repo", (Path.userHome / ".ivy2" / "test").asFile)
override def compileOptions = Nil
/** configuration of scripted testing **/
// Set to false to show logging as it happens without buffering, true to buffer until it completes and only show if the task fails.
// The output of scripted tasks executed in parallel will be inteleaved if true.
override def scriptedBufferLog = true
// Configure which versions of Scala to test against for those tests that do cross building
override def scriptedCompatibility = sbt.test.CompatibilityLevel.Full
def scalaVersionString = ScalaVersion.current.getOrElse(scalaVersion.value)
override def mainSources =

View File

@ -0,0 +1,6 @@
import sbt._
class Plugins(info: ProjectInfo) extends PluginDefinition(info)
{
val scripted = "org.scala-tools.sbt" % "test" % "0.6.5-p1"
}

View File

@ -2,7 +2,7 @@
#Sat Aug 01 10:32:20 EDT 2009
project.organization=org.scala-tools.sbt
project.name=test
sbt.version=0.5.4
project.version=0.5.4
sbt.version=0.5.6
project.version=0.6.5-p1
scala.version=2.7.5
project.initialize=false

View File

@ -6,6 +6,8 @@ import sbt._
class SbtTest(info: ProjectInfo) extends PluginProject(info)
{
val xsbtTest = "org.scala-tools.sbt" %% "test" % version.toString
val publishTo = "Scala Tools Nexus" at "http://nexus.scala-tools.org/content/repositories/releases/"
Credentials(Path.fromFile(System.getProperty("user.home")) / ".ivy2" / ".credentials", log)
}

View File

@ -0,0 +1,42 @@
package sbt.test
import java.io.{File, IOException}
import xsbt.IPC
import xsbt.test.{StatementHandler, TestFailed}
final class SbtHandler(directory: File, log: Logger, server: IPC.Server) extends StatementHandler
{
type State = Process
def initialState = newRemote
def apply(command: String, arguments: List[String], p: Process): Process =
{
send((command :: arguments).mkString(" "))
receive(command + " failed")
p
}
def finish(state: Process) =
try {
server.connection { _.send("exit") }
state.exitValue()
} catch {
case e: IOException => 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
}

View File

@ -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, _))

View File

@ -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)
}

View File

@ -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) =
<x>
sbt.version={sbtVersion}
def.scala.version={defScalaVersion}
build.scala.versions={buildScalaVersions}
</x>.text
}

View File

@ -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
}
}
}

View File

@ -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

View File

@ -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]
$ copy-file changes/TestProject7.scala project/build/src/TestProject.scala
> reload
> interactive-test
$ exists "ran"
$ exists "a/ran"
$ delete "ran"
$ delete "a/ran"

View File

@ -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"
}

View File

@ -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 }
}
}

View File

@ -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 }
})
}

View File

@ -1,11 +1,8 @@
> update
[success]
> +run 1
> +clean
> run
[success]
> +run 2
> +clean
> clean
[success]
> test
[success]
> +run -1
> +clean

View File

@ -1,2 +1 @@
> compile
[success]
> +compile

View File

@ -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

View File

@ -1,2 +1 @@
> compile
[success]
> +compile

View File

@ -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])
}

View File

@ -1,2 +1,7 @@
> compile
[failure]
# 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

View File

@ -1,9 +1,4 @@
> clean
[success]
> test
[success]
# this will fail if the sources are recompiled again (see the project definition)
> test
[success]
> test

View File

@ -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
}
}

View File

@ -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]
> +compile
> +check-output absent