From 64b19286ee2953ce7e6ffec882e240b514af6440 Mon Sep 17 00:00:00 2001 From: Mark Harrah Date: Sun, 13 Jun 2010 22:59:29 -0400 Subject: [PATCH] more reorganization, mostly IO. Also, move class file analyzer and history code to separate projects --- cache/Cache.scala | 12 +- cache/CacheIO.scala | 7 +- cache/FileInfo.scala | 16 +- cache/HListCache.scala | 17 +- cache/NoCache.scala | 5 +- cache/SeparatedCache.scala | 11 +- cache/src/test/scala/CacheTest.scala | 11 +- cache/tracking/ChangeReport.scala | 2 +- cache/tracking/DependencyTracking.scala | 13 +- cache/tracking/Tracked.scala | 14 +- cache/tracking/TrackingFormat.scala | 26 +-- .../src/main/java/xsbti/AnalysisCallback.java | 6 + interface/src/test/scala/TestCallback.scala | 2 + util/complete/History.scala | 49 ++++++ util/complete/HistoryCommands.scala | 84 +++++++++ util/control/ErrorHandling.scala | 4 +- util/log/src/test/scala/LogWriterTest.scala | 160 ++++++++++++++++++ 17 files changed, 382 insertions(+), 57 deletions(-) create mode 100644 util/complete/History.scala create mode 100644 util/complete/HistoryCommands.scala create mode 100644 util/log/src/test/scala/LogWriterTest.scala diff --git a/cache/Cache.scala b/cache/Cache.scala index e7ba310dc..c638e94f0 100644 --- a/cache/Cache.scala +++ b/cache/Cache.scala @@ -1,7 +1,11 @@ -package xsbt +/* sbt -- Simple Build Tool + * Copyright 2009 Mark Harrah + */ +package sbt import sbinary.{CollectionTypes, Format, JavaFormats} import java.io.File +import Types.:+: trait Cache[I,O] { @@ -44,13 +48,13 @@ trait BasicCacheImplicits extends NotNull new SeparatedCache(input, output) implicit def defaultEquiv[T]: Equiv[T] = new Equiv[T] { def equiv(a: T, b: T) = a == b } } -trait HListCacheImplicits extends HLists +trait HListCacheImplicits { - implicit def hConsInputCache[H,T<:HList](implicit headCache: InputCache[H], tailCache: InputCache[T]): InputCache[HCons[H,T]] = + implicit def hConsInputCache[H,T<:HList](implicit headCache: InputCache[H], tailCache: InputCache[T]): InputCache[H :+: T] = new HConsInputCache(headCache, tailCache) implicit lazy val hNilInputCache: InputCache[HNil] = new HNilInputCache - implicit def hConsOutputCache[H,T<:HList](implicit headCache: OutputCache[H], tailCache: OutputCache[T]): OutputCache[HCons[H,T]] = + implicit def hConsOutputCache[H,T<:HList](implicit headCache: OutputCache[H], tailCache: OutputCache[T]): OutputCache[H :+: T] = new HConsOutputCache(headCache, tailCache) implicit lazy val hNilOutputCache: OutputCache[HNil] = new HNilOutputCache } diff --git a/cache/CacheIO.scala b/cache/CacheIO.scala index e5c643c6a..7ff1eb519 100644 --- a/cache/CacheIO.scala +++ b/cache/CacheIO.scala @@ -1,4 +1,7 @@ -package xsbt +/* sbt -- Simple Build Tool + * Copyright 2009 Mark Harrah + */ +package sbt import java.io.{File, FileNotFoundException} import sbinary.{DefaultProtocol, Format, Operations} @@ -24,7 +27,7 @@ object CacheIO toFile(value)(file)(format, mf) def toFile[T](value: T)(file: File)(implicit format: Format[T], mf: Manifest[Format[T]]): Unit = { - FileUtilities.createDirectory(file.getParentFile) + IO.createDirectory(file.getParentFile) Operations.toFile(value)(file)(stampedFormat(format)) } def stampedFormat[T](format: Format[T])(implicit mf: Manifest[Format[T]]): Format[T] = diff --git a/cache/FileInfo.scala b/cache/FileInfo.scala index d1b350fa8..425a8598d 100644 --- a/cache/FileInfo.scala +++ b/cache/FileInfo.scala @@ -1,9 +1,11 @@ -package xsbt +/* sbt -- Simple Build Tool + * Copyright 2009 Mark Harrah + */ +package sbt import java.io.{File, IOException} import sbinary.{DefaultProtocol, Format} import DefaultProtocol._ -import Function.tupled import scala.reflect.Manifest sealed trait FileInfo extends NotNull @@ -43,22 +45,22 @@ object FileInfo type F = HashModifiedFileInfo implicit def apply(file: File): HashModifiedFileInfo = make(file, Hash(file).toList, file.lastModified) def make(file: File, hash: List[Byte], lastModified: Long): HashModifiedFileInfo = FileHashModified(file.getAbsoluteFile, hash, lastModified) - implicit val format: Format[HashModifiedFileInfo] = wrap(f => (f.file, f.hash, f.lastModified), tupled(make _)) + implicit val format: Format[HashModifiedFileInfo] = wrap(f => (f.file, f.hash, f.lastModified), (make _).tupled) } object hash extends Style { type F = HashFileInfo - implicit def apply(file: File): HashFileInfo = make(file, computeHash(file).toList) + implicit def apply(file: File): HashFileInfo = make(file, computeHash(file)) def make(file: File, hash: List[Byte]): HashFileInfo = FileHash(file.getAbsoluteFile, hash) - implicit val format: Format[HashFileInfo] = wrap(f => (f.file, f.hash), tupled(make _)) - private def computeHash(file: File) = try { Hash(file) } catch { case e: Exception => Nil } + implicit val format: Format[HashFileInfo] = wrap(f => (f.file, f.hash), (make _).tupled) + private def computeHash(file: File): List[Byte] = try { Hash(file).toList } catch { case e: Exception => Nil } } object lastModified extends Style { type F = ModifiedFileInfo implicit def apply(file: File): ModifiedFileInfo = make(file, file.lastModified) def make(file: File, lastModified: Long): ModifiedFileInfo = FileModified(file.getAbsoluteFile, lastModified) - implicit val format: Format[ModifiedFileInfo] = wrap(f => (f.file, f.lastModified), tupled(make _)) + implicit val format: Format[ModifiedFileInfo] = wrap(f => (f.file, f.lastModified), (make _).tupled) } object exists extends Style { diff --git a/cache/HListCache.scala b/cache/HListCache.scala index 90f00f47d..2bb3def3b 100644 --- a/cache/HListCache.scala +++ b/cache/HListCache.scala @@ -1,12 +1,15 @@ -package xsbt +/* sbt -- Simple Build Tool + * Copyright 2009 Mark Harrah + */ +package sbt import java.io.{InputStream,OutputStream} -import HLists._ +import Types._ class HNilInputCache extends NoInputCache[HNil] -class HConsInputCache[H,T <: HList](val headCache: InputCache[H], val tailCache: InputCache[T]) extends InputCache[HCons[H,T]] +class HConsInputCache[H,T <: HList](val headCache: InputCache[H], val tailCache: InputCache[T]) extends InputCache[H :+: T] { - def uptodate(in: HCons[H,T])(cacheStream: InputStream) = + def uptodate(in: H :+: T)(cacheStream: InputStream) = { val headResult = headCache.uptodate(in.head)(cacheStream) val tailResult = tailCache.uptodate(in.tail)(cacheStream) @@ -20,7 +23,7 @@ class HConsInputCache[H,T <: HList](val headCache: InputCache[H], val tailCache: } } } - def force(in: HCons[H,T])(cacheStream: OutputStream) = + def force(in: H :+: T)(cacheStream: OutputStream) = { headCache.force(in.head)(cacheStream) tailCache.force(in.tail)(cacheStream) @@ -28,7 +31,7 @@ class HConsInputCache[H,T <: HList](val headCache: InputCache[H], val tailCache: } class HNilOutputCache extends NoOutputCache[HNil](HNil) -class HConsOutputCache[H,T <: HList](val headCache: OutputCache[H], val tailCache: OutputCache[T]) extends OutputCache[HCons[H,T]] +class HConsOutputCache[H,T <: HList](val headCache: OutputCache[H], val tailCache: OutputCache[T]) extends OutputCache[H :+: T] { def loadCached(cacheStream: InputStream) = { @@ -36,7 +39,7 @@ class HConsOutputCache[H,T <: HList](val headCache: OutputCache[H], val tailCach val tail = tailCache.loadCached(cacheStream) HCons(head, tail) } - def update(out: HCons[H,T])(cacheStream: OutputStream) + def update(out: H :+: T)(cacheStream: OutputStream) { headCache.update(out.head)(cacheStream) tailCache.update(out.tail)(cacheStream) diff --git a/cache/NoCache.scala b/cache/NoCache.scala index a9cce3e99..bdb9c4f1b 100644 --- a/cache/NoCache.scala +++ b/cache/NoCache.scala @@ -1,4 +1,7 @@ -package xsbt +/* sbt -- Simple Build Tool + * Copyright 2009 Mark Harrah + */ +package sbt import java.io.{InputStream,OutputStream} diff --git a/cache/SeparatedCache.scala b/cache/SeparatedCache.scala index 91ecda713..6509e05bf 100644 --- a/cache/SeparatedCache.scala +++ b/cache/SeparatedCache.scala @@ -1,4 +1,7 @@ -package xsbt +/* sbt -- Simple Build Tool + * Copyright 2009 Mark Harrah + */ +package sbt import sbinary.Format import sbinary.JavaIO._ @@ -31,11 +34,11 @@ class SeparatedCache[I,O](input: InputCache[I], output: OutputCache[O]) extends catch { case _: Exception => Right(update(file)(in)) } protected def applyImpl(file: File, in: I) = { - OpenResource.fileInputStream(file) { stream => + Using.fileInputStream(file) { stream => val cache = input.uptodate(in)(stream) lazy val doUpdate = (result: O) => { - OpenResource.fileOutputStream(false)(file) { stream => + Using.fileOutputStream(false)(file) { stream => cache.update(stream) output.update(result)(stream) } @@ -49,7 +52,7 @@ class SeparatedCache[I,O](input: InputCache[I], output: OutputCache[O]) extends } protected def update(file: File)(in: I)(out: O) { - OpenResource.fileOutputStream(false)(file) { stream => + Using.fileOutputStream(false)(file) { stream => input.force(in)(stream) output.update(out)(stream) } diff --git a/cache/src/test/scala/CacheTest.scala b/cache/src/test/scala/CacheTest.scala index 65703ecaa..ad6085fc1 100644 --- a/cache/src/test/scala/CacheTest.scala +++ b/cache/src/test/scala/CacheTest.scala @@ -1,6 +1,7 @@ -package xsbt +package sbt import java.io.File +import Types.:+: object CacheTest// extends Properties("Cache test") { @@ -19,11 +20,11 @@ object CacheTest// extends Properties("Cache test") lazy val fileLength = length(create) - val c = cached(cCache) { (in: (File :: Long :: HNil)) => - val file :: len :: HNil = in + val c = cached(cCache) { (in: (File :+: Long :+: HNil)) => + val file :+: len :+: HNil = in println("File: " + file + " (" + file.exists + "), length: " + len) - (len+1) :: file :: HNil + (len+1) :+: file :+: HNil } - c(create :: fileLength :: HNil) + c(create :+: fileLength :+: HNil) } } \ No newline at end of file diff --git a/cache/tracking/ChangeReport.scala b/cache/tracking/ChangeReport.scala index c8f3a52eb..d25b1bbaa 100644 --- a/cache/tracking/ChangeReport.scala +++ b/cache/tracking/ChangeReport.scala @@ -1,7 +1,7 @@ /* sbt -- Simple Build Tool * Copyright 2009, 2010 Mark Harrah */ -package xsbt +package sbt object ChangeReport { diff --git a/cache/tracking/DependencyTracking.scala b/cache/tracking/DependencyTracking.scala index e34930f5a..30e060af3 100644 --- a/cache/tracking/DependencyTracking.scala +++ b/cache/tracking/DependencyTracking.scala @@ -1,7 +1,7 @@ /* sbt -- Simple Build Tool * Copyright 2009, 2010 Mark Harrah */ -package xsbt +package sbt private object DependencyTracking { @@ -24,7 +24,6 @@ trait UpdateTracking[T] extends NotNull // removes sources as keys/values in source, product maps and as values in reverseDependencies map def pending(sources: Iterable[T]): Unit } -import scala.collection.Set trait ReadTracking[T] extends NotNull { def isProduct(file: T): Boolean @@ -75,13 +74,13 @@ private abstract class DependencyTracking[T](translateProducts: Boolean) extends def isUsed(file: T): Boolean = exists(reverseUses, file) - final def allProducts = Set() ++ sourceMap.keys - final def allSources = Set() ++ productMap.keys - final def allUsed = Set() ++ reverseUses.keys + final def allProducts = sourceMap.keysIterator.toSet + final def allSources = productMap.keysIterator.toSet + final def allUsed = reverseUses.keysIterator.toSet final def allTags = tagMap.toSeq private def exists(map: DMap[T], value: T): Boolean = map.contains(value) - private def get(map: DMap[T], value: T): Set[T] = map.getOrElse(value, Set.empty[T]) + private def get(map: DMap[T], value: T): Set[T] = map.getOrElse[collection.Set[T]](value, Set.empty[T]).toSet final def dependency(sourceFile: T, dependsOn: T) { @@ -89,7 +88,7 @@ private abstract class DependencyTracking[T](translateProducts: Boolean) extends if(!translateProducts) Seq(dependsOn) else - sourceMap.getOrElse(dependsOn, Seq(dependsOn)) + sourceMap.getOrElse[Iterable[T]](dependsOn, Seq(dependsOn)) actualDependencies.foreach { actualDependency => reverseDependencies.add(actualDependency, sourceFile) } } final def product(sourceFile: T, product: T) diff --git a/cache/tracking/Tracked.scala b/cache/tracking/Tracked.scala index 49d33c622..77c7447f5 100644 --- a/cache/tracking/Tracked.scala +++ b/cache/tracking/Tracked.scala @@ -1,13 +1,13 @@ /* sbt -- Simple Build Tool * Copyright 2009, 2010 Mark Harrah */ -package xsbt +package sbt import java.io.{File,IOException} import CacheIO.{fromFile, toFile} import sbinary.Format import scala.reflect.Manifest -import xsbt.FileUtilities.{delete, read, write} +import IO.{delete, read, write} /* A proper implementation of fileTask that tracks inputs and outputs properly @@ -34,7 +34,7 @@ object Tracked * If 'useStartTime' is true, the recorded time is the start of the evaluated function. * If 'useStartTime' is false, the recorded time is when the evaluated function completes. * In both cases, the timestamp is not updated if the function throws an exception.*/ - def tstamp(cacheFile: File, useStartTime: Boolean): Timestamp = new Timestamp(cacheFile) + def tstamp(cacheFile: File, useStartTime: Boolean = true): Timestamp = new Timestamp(cacheFile, useStartTime) /** Creates a tracker that only evaluates a function when the input has changed.*/ def changed[O](cacheFile: File)(getValue: => O)(implicit input: InputCache[O]): Changed[O] = new Changed[O](getValue, cacheFile) @@ -76,13 +76,13 @@ class Changed[O](getValue: => O, val cacheFile: File)(implicit input: InputCache { val value = getValue val cache = - try { OpenResource.fileInputStream(cacheFile)(input.uptodate(value)) } + try { Using.fileInputStream(cacheFile)(input.uptodate(value)) } catch { case _: IOException => new ForceResult(input)(value) } if(cache.uptodate) ifUnchanged(value) else { - OpenResource.fileOutputStream(false)(cacheFile)(cache.update) + Using.fileOutputStream(false)(cacheFile)(cache.update) ifChanged(value) } } @@ -108,7 +108,7 @@ class Difference(getFiles: => Set[File], val style: FilesInfo.Style, val cache: if(defineClean) delete(raw(cachedFilesInfo)) else () clearCache() } - private def clearCache = delete(cache) + private def clearCache() = delete(cache) private def cachedFilesInfo = fromFile(style.formats, style.empty)(cache)(style.manifest).files private def raw(fs: Set[style.F]): Set[File] = fs.map(_.file) @@ -161,7 +161,7 @@ object InvalidateFiles def apply(cacheDirectory: File, translateProducts: Boolean): InvalidateTransitive[File] = { import sbinary.DefaultProtocol.FileFormat - new InvalidateTransitive[File](cacheDirectory, translateProducts, FileUtilities.delete) + new InvalidateTransitive[File](cacheDirectory, translateProducts, IO.delete) } } diff --git a/cache/tracking/TrackingFormat.scala b/cache/tracking/TrackingFormat.scala index 1318c6096..d8a5e0f2c 100644 --- a/cache/tracking/TrackingFormat.scala +++ b/cache/tracking/TrackingFormat.scala @@ -1,7 +1,7 @@ /* sbt -- Simple Build Tool * Copyright 2009, 2010 Mark Harrah */ -package xsbt +package sbt import java.io.File import scala.collection.mutable.{HashMap, Map, MultiMap, Set} @@ -34,16 +34,22 @@ private class TrackingFormat[T](directory: File, translateProducts: Boolean)(imp } private object TrackingFormat { - implicit def mutableMapFormat[S, T](implicit binS : Format[S], binT : Format[T]) : Format[Map[S, T]] = - viaArray( (x : Array[(S, T)]) => Map(x :_*)); - implicit def depMapFormat[T](implicit bin: Format[T]) : Format[DMap[T]] = - { - viaArray { (x : Array[(T, Set[T])]) => - val map = newMap[T] - map ++= x - map + implicit def mutableMapFormat[S, T](implicit binS : Format[S], binT : Format[T]) : Format[HashMap[S, T]] = + new LengthEncoded[HashMap[S, T], (S, T)] { + def build(size : Int, ts : Iterator[(S, T)]) : HashMap[S, T] = { + val b = new HashMap[S, T] + b ++= ts + b + } + } + implicit def depMapFormat[T](implicit bin: Format[T]) : Format[DMap[T]] = + new LengthEncoded[DMap[T], (T, Set[T])] { + def build(size : Int, ts : Iterator[(T, Set[T])]) : DMap[T] = { + val b = newMap[T] + b ++= ts + b + } } - } def trackingFormat[T](translateProducts: Boolean)(implicit tFormat: Format[T]): Format[DependencyTracking[T]] = asProduct4((a: DMap[T],b: DMap[T],c: DMap[T], d:TagMap[T]) => new DefaultTracking(translateProducts)(a,b,c,d) : DependencyTracking[T] )(dt => (dt.reverseDependencies, dt.reverseUses, dt.sourceMap, dt.tagMap)) diff --git a/interface/src/main/java/xsbti/AnalysisCallback.java b/interface/src/main/java/xsbti/AnalysisCallback.java index 4db5f28c3..03c4798c9 100644 --- a/interface/src/main/java/xsbti/AnalysisCallback.java +++ b/interface/src/main/java/xsbti/AnalysisCallback.java @@ -31,6 +31,12 @@ public interface AnalysisCallback /** Called to indicate that the source file source depends on the class file * clazz.*/ public void classDependency(File clazz, File source); + /** Called to indicate that the source file sourcePath depends on the class file + * classFile that is a product of some source. This differs from classDependency + * because it is really a sourceDependency. The source corresponding to classFile + * was not incuded in the compilation so the plugin doesn't know what the source is though. It + * only knows that the class file came from the output directory.*/ + public void productDependency(File classFile, File sourcePath); /** Called to indicate that the source file source produces a class file at * module.*/ public void generatedClass(File source, File module); diff --git a/interface/src/test/scala/TestCallback.scala b/interface/src/test/scala/TestCallback.scala index 8e7014af7..75e8d77af 100644 --- a/interface/src/test/scala/TestCallback.scala +++ b/interface/src/test/scala/TestCallback.scala @@ -13,6 +13,7 @@ class TestCallback(val superclassNames: Array[String], val annotationNames: Arra val sourceDependencies = new ArrayBuffer[(File, File)] val jarDependencies = new ArrayBuffer[(File, File)] val classDependencies = new ArrayBuffer[(File, File)] + val productDependencies = new ArrayBuffer[(File, File)] val products = new ArrayBuffer[(File, File)] val applications = new ArrayBuffer[(File, String)] @@ -25,6 +26,7 @@ class TestCallback(val superclassNames: Array[String], val annotationNames: Arra def sourceDependency(dependsOn: File, source: File) { sourceDependencies += ((dependsOn, source)) } def jarDependency(jar: File, source: File) { jarDependencies += ((jar, source)) } def classDependency(clazz: File, source: File) { classDependencies += ((clazz, source)) } + def productDependency(clazz: File, source: File) { productDependencies += ((clazz, source)) } def generatedClass(source: File, module: File) { products += ((source, module)) } def endSource(source: File) { endedSources += source } def foundApplication(source: File, className: String) { applications += ((source, className)) } diff --git a/util/complete/History.scala b/util/complete/History.scala new file mode 100644 index 000000000..bf009f626 --- /dev/null +++ b/util/complete/History.scala @@ -0,0 +1,49 @@ +/* sbt -- Simple Build Tool + * Copyright 2010 Mark Harrah + */ +package sbt +package complete + +import History.number + +final class History private(lines: IndexedSeq[String], error: (=> String) => Unit) extends NotNull +{ + private def reversed = lines.reverse + + def all: Seq[String] = lines + def size = lines.length + def !! : Option[String] = !- (1) + def apply(i: Int): Option[String] = if(0 <= i && i < size) Some( lines(i) ) else { error("Invalid history index: " + i); None } + def !(i: Int): Option[String] = apply(i) + + def !(s: String): Option[String] = + number(s) match + { + case Some(n) => if(n < 0) !- (-n) else apply(n) + case None => nonEmpty(s) { reversed.find(_.startsWith(s)) } + } + def !- (n: Int): Option[String] = apply(size - n - 1) + + def !?(s: String): Option[String] = nonEmpty(s) { reversed.drop(1).find(_.contains(s)) } + + private def nonEmpty[T](s: String)(act: => Option[T]): Option[T] = + if(s.isEmpty) + { + error("No action specified to history command") + None + } + else + act + + def list(historySize: Int, show: Int): Seq[String] = + lines.toList.drop((lines.size - historySize) max 0).zipWithIndex.map { case (line, number) => " " + number + " " + line }.takeRight(show max 1) +} + +object History +{ + def apply(lines: Seq[String], error: (=> String) => Unit): History = new History(lines.toIndexedSeq, error) + + def number(s: String): Option[Int] = + try { Some(s.toInt) } + catch { case e: NumberFormatException => None } +} \ No newline at end of file diff --git a/util/complete/HistoryCommands.scala b/util/complete/HistoryCommands.scala new file mode 100644 index 000000000..a03c9cfca --- /dev/null +++ b/util/complete/HistoryCommands.scala @@ -0,0 +1,84 @@ +/* sbt -- Simple Build Tool + * Copyright 2010 Mark Harrah + */ +package sbt +package complete + +object HistoryCommands +{ + val Start = "!" + // second characters + val Contains = "?" + val Last = "!" + val ListCommands = ":" + + def ContainsFull = h(Contains) + def LastFull = h(Last) + def ListFull = h(ListCommands) + + def ListN = ListFull + "n" + def ContainsString = ContainsFull + "string" + def StartsWithString = Start + "string" + def Previous = Start + "-n" + def Nth = Start + "n" + + private def h(s: String) = Start + s + def plainCommands = Seq(ListFull, Start, LastFull, ContainsFull) + + def descriptions = Seq( + LastFull -> "Execute the last command again", + ListFull -> "Show all previous commands", + ListN -> "Show the last n commands", + Nth -> ("Execute the command with index n, as shown by the " + ListFull + " command"), + Previous -> "Execute the nth command before this one", + StartsWithString -> "Execute the most recent command starting with 'string'", + ContainsString -> "Execute the most recent command containing 'string'" + ) + def helpString = "History commands:\n " + (descriptions.map{ case (c,d) => c + " " + d}).mkString("\n ") + def printHelp(): Unit = + println(helpString) + + def apply(s: String, historyPath: Option[Path], maxLines: Int, error: (=> String) => Unit): Option[List[String]] = + if(s.isEmpty) + { + printHelp() + Some(Nil) + } + else + { + val lines = historyPath.toList.flatMap(h => IO.readLines(h.asFile) ).toArray + if(lines.isEmpty) + { + error("No history") + None + } + else + { + val history = complete.History(lines, error) + if(s.startsWith(ListCommands)) + { + val rest = s.substring(ListCommands.length) + val show = complete.History.number(rest).getOrElse(lines.length) + printHistory(history, maxLines, show) + Some(Nil) + } + else + { + val command = historyCommand(history, s) + command.foreach(lines(lines.length - 1) = _) + historyPath foreach { h => IO.writeLines(h.asFile, lines) } + Some(command.toList) + } + } + } + def printHistory(history: complete.History, historySize: Int, show: Int): Unit = history.list(historySize, show).foreach(println) + def historyCommand(history: complete.History, s: String): Option[String] = + { + if(s == Last) + history !! + else if(s.startsWith(Contains)) + history !? s.substring(Contains.length) + else + history ! s + } +} \ No newline at end of file diff --git a/util/control/ErrorHandling.scala b/util/control/ErrorHandling.scala index f4d42993b..4072edff3 100644 --- a/util/control/ErrorHandling.scala +++ b/util/control/ErrorHandling.scala @@ -1,7 +1,7 @@ /* sbt -- Simple Build Tool * Copyright 2009 Mark Harrah */ -package xsbt +package sbt object ErrorHandling { @@ -22,7 +22,7 @@ object ErrorHandling try { Right(f) } catch { case e: Exception => Left(e) } } -final class TranslatedException private[xsbt](msg: String, cause: Throwable) extends RuntimeException(msg, cause) +final class TranslatedException private[sbt](msg: String, cause: Throwable) extends RuntimeException(msg, cause) { override def toString = msg } \ No newline at end of file diff --git a/util/log/src/test/scala/LogWriterTest.scala b/util/log/src/test/scala/LogWriterTest.scala new file mode 100644 index 000000000..6d01341a0 --- /dev/null +++ b/util/log/src/test/scala/LogWriterTest.scala @@ -0,0 +1,160 @@ +/* sbt -- Simple Build Tool + * Copyright 2010 Mark Harrah */ + +package sbt + +import org.scalacheck._ +import Arbitrary.{arbitrary => arb, _} +import Gen.{listOfN, oneOf} +import Prop._ + +import java.io.Writer + +object LogWriterTest extends Properties("Log Writer") +{ + final val MaxLines = 100 + final val MaxSegments = 10 + + /* 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 <- listOf[ToLog](MaxSegments); 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(Level.values.toSeq) + + implicit lazy val genOutput: Gen[Output] = + for(ls <- listOf[List[ToLog]](MaxLines); lv <- genLevel) yield + new Output(ls, lv) + + 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 + + def listOf[T](max: Int)(implicit content: Arbitrary[T]): Gen[List[T]] = + Gen.choose(0, max) flatMap { sz => listOfN(sz, content.arbitrary) } +} + +/* 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