diff --git a/compile/src/test/scala/CompileTest.scala b/compile/src/test/scala/CompileTest.scala index 5b5ffe400..cbaf1266d 100644 --- a/compile/src/test/scala/CompileTest.scala +++ b/compile/src/test/scala/CompileTest.scala @@ -16,7 +16,7 @@ object CompileTest extends Specification WithCompiler( "2.7.5" )(testCompileAnalysis) WithCompiler( "2.7.7" )(testCompileAnalysis) WithCompiler( "2.8.0.Beta1" )(testCompileAnalysis) - //WithCompiler( "2.8.0-SNAPSHOT" )(testCompileAnalysis) + WithCompiler( "2.8.0-SNAPSHOT" )(testCompileAnalysis) } } diff --git a/compile/src/test/scala/TestCompile.scala b/compile/src/test/scala/TestCompile.scala index b76d39f3d..76ef4537a 100644 --- a/compile/src/test/scala/TestCompile.scala +++ b/compile/src/test/scala/TestCompile.scala @@ -8,7 +8,7 @@ import FileUtilities.withTemporaryDirectory object TestCompile { // skip 2.7.3 and 2.7.4 for speed - def allVersions = List("2.7.2", "2.7.5", "2.7.7", "2.8.0.Beta1")//List("2.7.2", "2.7.3", "2.7.4", "2.7.5", "2.8.0-SNAPSHOT") + def allVersions = List("2.7.2", "2.7.5", "2.7.7", "2.8.0.Beta1", "2.8.0-SNAPSHOT")//List("2.7.2", "2.7.3", "2.7.4", "2.7.5", "2.8.0-SNAPSHOT") /** Tests running the compiler interface with the analyzer plugin with a test callback. The test callback saves all information * that the plugin sends it for post-compile analysis by the provided function.*/ def apply[T](scalaVersion: String, sources: Set[File], outputDirectory: File, options: Seq[String], superclassNames: Seq[String], annotationNames: Seq[String]) diff --git a/sbt/src/main/scala/sbt/Compile.scala b/sbt/src/main/scala/sbt/Compile.scala index fdd2209fa..406d5b1e4 100644 --- a/sbt/src/main/scala/sbt/Compile.scala +++ b/sbt/src/main/scala/sbt/Compile.scala @@ -1,5 +1,5 @@ /* sbt -- Simple Build Tool - * Copyright 2008, 2009 Mark Harrah + * Copyright 2008, 2009 Mark Harrah, Seth Tisue */ package sbt @@ -79,11 +79,30 @@ final class Compile(maximumErrors: Int, compiler: AnalyzingCompiler, analysisCal { val augmentedClasspath = if(compiler.autoBootClasspath) classpath + compiler.scalaInstance.libraryJar else classpath val arguments = (new CompilerArguments(compiler.scalaInstance, false, compiler.compilerOnClasspath))(sources, augmentedClasspath, outputDirectory, options) - log.debug("Calling 'javac' with arguments:\n\t" + arguments.mkString("\n\t")) - def javac(argFile: File) = Process("javac", ("@" + normalizeSlash(argFile.getAbsolutePath)) :: Nil) ! log - val code = withArgumentFile(arguments)(javac) + log.debug("running javac with arguments:\n\t" + arguments.mkString("\n\t")) + val code: Int = + try { directJavac(arguments, log) } + catch { case e: ClassNotFoundException => forkJavac(arguments, log) } + log.debug("javac returned exit code: " + code) if( code != 0 ) throw new CompileFailed(arguments.toArray, "javac returned nonzero exit code") } + private def forkJavac(arguments: Seq[String], log: Logger): Int = + { + log.debug("com.sun.tools.javac.Main not found; forking javac instead") + def externalJavac(argFile: File) = Process("javac", ("@" + normalizeSlash(argFile.getAbsolutePath)) :: Nil) ! log + withArgumentFile(arguments)(externalJavac) + } + private def directJavac(arguments: Seq[String], log: Logger): Int = + { + val writer = new java.io.PrintWriter(new LoggerWriter(log, Level.Error)) + val argsArray = arguments.toArray + val javac = Class.forName("com.sun.tools.javac.Main") + log.debug("Calling javac directly.") + javac.getDeclaredMethod("compile", classOf[Array[String]], classOf[java.io.PrintWriter]) + .invoke(null, argsArray, writer) + .asInstanceOf[java.lang.Integer] + .intValue + } } trait WithArgumentFile extends NotNull { diff --git a/sbt/src/main/scala/sbt/Logger.scala b/sbt/src/main/scala/sbt/Logger.scala index dfd3be94f..579bde171 100644 --- a/sbt/src/main/scala/sbt/Logger.scala +++ b/sbt/src/main/scala/sbt/Logger.scala @@ -1,5 +1,5 @@ /* sbt -- Simple Build Tool - * Copyright 2008, 2009 Mark Harrah + * Copyright 2008, 2009, 2010 Mark Harrah */ package sbt @@ -355,10 +355,38 @@ object Level extends Enumeration with NotNull /** Same as apply, defined for use in pattern matching. */ private[sbt] def unapply(s: String) = apply(s) } -final class LoggerWriter(delegate: Logger, level: Level.Value) extends java.io.Writer +/** Provides a `java.io.Writer` interface to a `Logger`. Content is line-buffered and logged at `level`. +* A line is delimited by `nl`, which is by default the platform line separator.*/ +final class LoggerWriter(delegate: Logger, level: Level.Value, nl: String) extends java.io.Writer { - override def flush() {} - override def close() {} + def this(delegate: Logger, level: Level.Value) = this(delegate, level, FileUtilities.Newline) + + private[this] val buffer = new StringBuilder + + override def close() = flush() + override def flush(): Unit = + synchronized { + if(buffer.length > 0) + { + log(buffer.toString) + buffer.clear() + } + } override def write(content: Array[Char], offset: Int, length: Int): Unit = - delegate.log(level, new String(content, offset, length)) + synchronized { + buffer.append(content, offset, length) + process() + } + + private[this] def process() + { + val i = buffer.indexOf(nl) + if(i >= 0) + { + log(buffer.substring(0, i)) + buffer.delete(0, i + nl.length) + process() + } + } + private[this] def log(s: String): Unit = delegate.log(level, s) } \ No newline at end of file diff --git a/sbt/src/sbt-test/tests/it/changes/ClassFailModuleFail.scala b/sbt/src/sbt-test/tests/it/changes/ClassFailModuleFail.scala new file mode 100644 index 000000000..002c8ddbf --- /dev/null +++ b/sbt/src/sbt-test/tests/it/changes/ClassFailModuleFail.scala @@ -0,0 +1,15 @@ +import org.specs._ + +class A extends Specification +{ + "this" should { + "not work" in { 1 must_== 2 } + } +} + +object A extends Specification +{ + "this" should { + "not work" in { 1 must_== 2 } + } +} \ No newline at end of file diff --git a/sbt/src/sbt-test/tests/it/changes/ClassFailModuleSuccess.scala b/sbt/src/sbt-test/tests/it/changes/ClassFailModuleSuccess.scala new file mode 100644 index 000000000..8c3b7eb0f --- /dev/null +++ b/sbt/src/sbt-test/tests/it/changes/ClassFailModuleSuccess.scala @@ -0,0 +1,15 @@ +import org.specs._ + +class A extends Specification +{ + "this" should { + "not work" in { 1 must_== 2 } + } +} + +object A extends Specification +{ + "this" should { + "work" in { 1 must_== 1 } + } +} \ No newline at end of file diff --git a/sbt/src/sbt-test/tests/it/changes/ClassSuccessModuleFail.scala b/sbt/src/sbt-test/tests/it/changes/ClassSuccessModuleFail.scala new file mode 100644 index 000000000..37801473b --- /dev/null +++ b/sbt/src/sbt-test/tests/it/changes/ClassSuccessModuleFail.scala @@ -0,0 +1,15 @@ +import org.specs._ + +class A extends Specification +{ + "this" should { + "work" in { 1 must_== 1 } + } +} + +object A extends Specification +{ + "this" should { + "not work" in { 1 must_== 2 } + } +} \ No newline at end of file diff --git a/sbt/src/sbt-test/tests/it/changes/ClassSuccessModuleSuccess.scala b/sbt/src/sbt-test/tests/it/changes/ClassSuccessModuleSuccess.scala new file mode 100644 index 000000000..2827d30ea --- /dev/null +++ b/sbt/src/sbt-test/tests/it/changes/ClassSuccessModuleSuccess.scala @@ -0,0 +1,15 @@ +import org.specs._ + +class A extends Specification +{ + "this" should { + "work" in { 1 must_== 1 } + } +} + +object A extends Specification +{ + "this" should { + "work" in { 1 must_== 1 } + } +} \ No newline at end of file diff --git a/sbt/src/sbt-test/tests/it/project/build.properties b/sbt/src/sbt-test/tests/it/project/build.properties new file mode 100644 index 000000000..fe72372e7 --- /dev/null +++ b/sbt/src/sbt-test/tests/it/project/build.properties @@ -0,0 +1,2 @@ +project.name=Specs Test Type Test +project.version=1.0 \ No newline at end of file diff --git a/sbt/src/sbt-test/tests/it/project/build/TestProject.scala b/sbt/src/sbt-test/tests/it/project/build/TestProject.scala new file mode 100644 index 000000000..6796bb2e3 --- /dev/null +++ b/sbt/src/sbt-test/tests/it/project/build/TestProject.scala @@ -0,0 +1,6 @@ +import sbt._ + +class TestProject(info: ProjectInfo) extends DefaultProject(info) with BasicScalaIntegrationTesting +{ + val specs = "org.scala-tools.testing" %% "specs" % "1.6.1" intransitive() +} \ No newline at end of file diff --git a/sbt/src/sbt-test/tests/it/test b/sbt/src/sbt-test/tests/it/test new file mode 100644 index 000000000..acdeebc15 --- /dev/null +++ b/sbt/src/sbt-test/tests/it/test @@ -0,0 +1,24 @@ +#> ++2.8.0.Beta1-RC7 +> ++2.7.7 +> update + +> clean +$ delete src/ +$ copy-file changes/ClassFailModuleSuccess.scala src/it/scala/Test.scala +-> integration-test + +> clean +$ delete src/ +$ copy-file changes/ClassFailModuleFail.scala src/it/scala/Test.scala +-> integration-test + +> clean +$ delete src/ +$ copy-file changes/ClassSuccessModuleFail.scala src/it/scala/Test.scala +-> integration-test + + +> clean +$ delete src/ +$ copy-file changes/ClassSuccessModuleSuccess.scala src/it/scala/Test.scala +> integration-test \ No newline at end of file diff --git a/sbt/src/test/scala/sbt/LogWriterTest.scala b/sbt/src/test/scala/sbt/LogWriterTest.scala new file mode 100644 index 000000000..ec03eeca2 --- /dev/null +++ b/sbt/src/test/scala/sbt/LogWriterTest.scala @@ -0,0 +1,156 @@ +/* sbt -- Simple Build Tool + * Copyright 2010 Mark Harrah */ + +package sbt + +import org.scalacheck._ +import Arbitrary.{arbitrary => arb, _} +import Gen.{oneOf, value} +import Prop._ + +import java.io.Writer + +object LogWriterTest extends Properties("Log Writer") +{ + /* Tests that content written through a LoggerWriter is properly passed to the underlying Logger. + * Each line, determined by the specified newline separator, must be logged at the correct logging level. */ + property("properly logged") = forAll { (output: Output, newLine: NewLine) => + import output.{lines, level} + val log = new RecordingLogger + val writer = new LoggerWriter(log, level, newLine.str) + logLines(writer, lines, newLine.str) + val events = log.getEvents + ("Recorded:\n" + events.map(show).mkString("\n")) |: + check( toLines(lines), events, level) + } + + /** Displays a LogEvent in a useful format for debugging. In particular, we are only interested in `Log` types + * and non-printable characters should be escaped*/ + def show(event: LogEvent): String = + event match + { + case l: Log => "Log('" + Escape(l.msg) + "', " + l.level + ")" + case _ => "Not Log" + } + /** Writes the given lines to the Writer. `lines` is taken to be a list of lines, which are + * represented as separately written segments (ToLog instances). ToLog.`byCharacter` + * indicates whether to write the segment by character (true) or all at once (false)*/ + def logLines(writer: Writer, lines: List[List[ToLog]], newLine: String) + { + for(line <- lines; section <- line) + { + val content = section.content + val normalized = Escape.newline(content, newLine) + if(section.byCharacter) + normalized.foreach { c => writer.write(c.toInt) } + else + writer.write(normalized) + } + writer.flush() + } + + /** Converts the given lines in segments to lines as Strings for checking the results of the test.*/ + def toLines(lines: List[List[ToLog]]): List[String] = + lines.map(_.map(_.contentOnly).mkString) + /** Checks that the expected `lines` were recorded as `events` at level `Lvl`.*/ + def check(lines: List[String], events: List[LogEvent], Lvl: Level.Value): Boolean = + (lines zip events) forall { + case (line, log : Log) => log.level == Lvl && line == log.msg + case _ => false + } + + /* The following are implicit generators to build up a write sequence. + * ToLog represents a written segment. NewLine represents one of the possible + * newline separators. A List[ToLog] represents a full line and always includes a + * final ToLog with a trailing '\n'. Newline characters are otherwise not present in + * the `content` of a ToLog instance.*/ + + implicit lazy val arbOut: Arbitrary[Output] = Arbitrary(genOutput) + implicit lazy val arbLog: Arbitrary[ToLog] = Arbitrary(genLog) + implicit lazy val arbLine: Arbitrary[List[ToLog]] = Arbitrary(genLine) + implicit lazy val arbNewLine: Arbitrary[NewLine] = Arbitrary(genNewLine) + implicit lazy val arbLevel : Arbitrary[Level.Value] = Arbitrary(genLevel) + + implicit def genLine(implicit logG: Gen[ToLog]): Gen[List[ToLog]] = + for(l <- arbList[ToLog].arbitrary; last <- logG) yield + (addNewline(last) :: l.filter(!_.content.isEmpty)).reverse + + implicit def genLog(implicit content: Arbitrary[String], byChar: Arbitrary[Boolean]): Gen[ToLog] = + for(c <- content.arbitrary; by <- byChar.arbitrary) yield + { + assert(c != null) + new ToLog(removeNewlines(c), by) + } + + implicit lazy val genNewLine: Gen[NewLine] = + for(str <- oneOf("\n", "\r", "\r\n")) yield + new NewLine(str) + + implicit lazy val genLevel: Gen[Level.Value] = + oneOf(levelsGen : _*) + + implicit lazy val genOutput: Gen[Output] = + for(ls <- arbList[List[ToLog]].arbitrary; lv <- genLevel) yield + new Output(ls, lv) + + def levelsGen: Seq[Gen[Level.Value]] = Level.elements.toList.map(x => value(x)) + + def removeNewlines(s: String) = s.replaceAll("""[\n\r]+""", "") + def addNewline(l: ToLog): ToLog = + new ToLog(l.content + "\n", l.byCharacter) // \n will be replaced by a random line terminator for all lines +} + +/* Helper classes*/ + +final class Output(val lines: List[List[ToLog]], val level: Level.Value) extends NotNull +{ + override def toString = + "Level: " + level + "\n" + lines.map(_.mkString).mkString("\n") +} +final class NewLine(val str: String) extends NotNull +{ + override def toString = Escape(str) +} +final class ToLog(val content: String, val byCharacter: Boolean) extends NotNull +{ + def contentOnly = Escape.newline(content, "") + override def toString = if(content.isEmpty) "" else "ToLog('" + Escape(contentOnly) + "', " + byCharacter + ")" +} +/** Defines some utility methods for escaping unprintable characters.*/ +object Escape +{ + /** Escapes characters with code less than 20 by printing them as unicode escapes.*/ + def apply(s: String): String = + { + val builder = new StringBuilder(s.length) + for(c <- s) + { + def escaped = pad(c.toInt.toHexString.toUpperCase, 4, '0') + if(c < 20) builder.append("\\u").append(escaped) else builder.append(c) + } + builder.toString + } + def pad(s: String, minLength: Int, extra: Char) = + { + val diff = minLength - s.length + if(diff <= 0) s else List.make(diff, extra).mkString("", "", s) + } + /** Replaces a \n character at the end of a string `s` with `nl`.*/ + def newline(s: String, nl: String): String = + if(s.endsWith("\n")) s.substring(0, s.length - 1) + nl else s +} +/** Records logging events for later retrieval.*/ +final class RecordingLogger extends BasicLogger +{ + private var events: List[LogEvent] = Nil + + def getEvents = events.reverse + + override def ansiCodesSupported = true + def trace(t: => Throwable) { events ::= new Trace(t) } + def log(level: Level.Value, message: => String) { events ::= new Log(level, message) } + def success(message: => String) { events ::= new Success(message) } + def logAll(es: Seq[LogEvent]) { events :::= es.toList } + def control(event: ControlEvent.Value, message: => String) { events ::= new ControlEvent(event, message) } + +} \ No newline at end of file diff --git a/sbt/src/test/scala/sbt/PathSpecification.scala b/sbt/src/test/scala/sbt/PathSpecification.scala index d51cb94f7..be3123f0c 100644 --- a/sbt/src/test/scala/sbt/PathSpecification.scala +++ b/sbt/src/test/scala/sbt/PathSpecification.scala @@ -1,5 +1,5 @@ /* sbt -- Simple Build Tool - * Copyright 2008 Mark Harrah + * Copyright 2008, 2010 Mark Harrah */ package sbt