Caching based on sjsonnew

This commit is contained in:
Martin Duhem 2016-06-27 15:27:42 +02:00
parent d760b15193
commit 4cffccc8c8
20 changed files with 1037 additions and 599 deletions

View File

@ -111,22 +111,24 @@ lazy val utilLogic = (project in internalPath / "util-logic").
name := "Util Logic"
)
// Persisted caching based on SBinary
// Persisted caching based on sjson-new
lazy val utilCache = (project in internalPath / "util-cache").
dependsOn(utilCollection).
dependsOn(utilCollection, utilTesting % Test).
settings(
commonSettings,
name := "Util Cache",
libraryDependencies ++= Seq(sbinary, sbtSerialization, scalaReflect.value, sbtIO) ++ scalaXml.value
libraryDependencies ++= Seq(datatypeCodecs, sbtSerialization, scalaReflect.value, sbtIO) ++ scalaXml.value,
libraryDependencies += sjsonnewScalaJson % Test
)
// Builds on cache to provide caching for filesystem-related operations
lazy val utilTracking = (project in internalPath / "util-tracking").
dependsOn(utilCache).
dependsOn(utilCache, utilTesting % Test).
settings(
commonSettings,
name := "Util Tracking",
libraryDependencies += sbtIO
libraryDependencies += sbtIO,
libraryDependencies += sjsonnewScalaJson % Test
)
// Internal utility for testing

View File

@ -0,0 +1,52 @@
package sbt.internal.util
import sbt.datatype.StringFormat
import sbt.internal.util.Types.:+:
import sjsonnew.{ Builder, deserializationError, JsonFormat, Unbuilder }
import sjsonnew.BasicJsonProtocol.{ wrap, asSingleton }
import java.io.File
import java.net.{ URI, URL }
trait URIFormat { self: StringFormat =>
implicit def URIFormat: JsonFormat[URI] = wrap(_.toString, new URI(_: String))
}
trait URLFormat { self: StringFormat =>
implicit def URLFormat: JsonFormat[URL] = wrap(_.toString, new URL(_: String))
}
trait FileFormat { self: StringFormat =>
implicit def FileFormat: JsonFormat[File] = wrap(_.toString, new File(_: String))
}
trait HListFormat {
implicit def HConsFormat[H: JsonFormat, T <: HList: JsonFormat]: JsonFormat[H :+: T] =
new JsonFormat[H :+: T] {
override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): H :+: T =
jsOpt match {
case Some(js) =>
unbuilder.beginObject(js)
val h = unbuilder.readField[H]("h")
val t = unbuilder.readField[T]("t")
unbuilder.endObject()
HCons(h, t)
case None =>
deserializationError("Expect JValue but found None")
}
override def write[J](obj: H :+: T, builder: Builder[J]): Unit = {
builder.beginObject()
builder.addField("h", obj.head)
builder.addField("t", obj.tail)
builder.endObject()
}
}
implicit val HNilFormat: JsonFormat[HNil] = asSingleton(HNil)
}

View File

@ -0,0 +1,59 @@
package sbt.internal.util
import sbt.datatype.{ ArrayFormat, BooleanFormat, ByteFormat, IntFormat }
import java.net.{ URI, URL }
import sjsonnew.JsonFormat
import sjsonnew.BasicJsonProtocol.asSingleton
trait BasicCacheImplicits { self: ArrayFormat with BooleanFormat with ByteFormat with IntFormat =>
implicit def basicCache[I: JsonFormat: Equiv, O: JsonFormat]: Cache[I, O] =
new BasicCache[I, O]()
def defaultEquiv[T]: Equiv[T] =
new Equiv[T] { def equiv(a: T, b: T) = a == b }
def wrapEquiv[S, T](f: S => T)(implicit eqT: Equiv[T]): Equiv[S] =
new Equiv[S] {
def equiv(a: S, b: S) =
eqT.equiv(f(a), f(b))
}
implicit def optEquiv[T](implicit t: Equiv[T]): Equiv[Option[T]] =
new Equiv[Option[T]] {
def equiv(a: Option[T], b: Option[T]) =
(a, b) match {
case (None, None) => true
case (Some(va), Some(vb)) => t.equiv(va, vb)
case _ => false
}
}
implicit def urlEquiv(implicit uriEq: Equiv[URI]): Equiv[URL] = wrapEquiv[URL, URI](_.toURI)(uriEq)
implicit def uriEquiv: Equiv[URI] = defaultEquiv
implicit def stringSetEquiv: Equiv[Set[String]] = defaultEquiv
implicit def stringMapEquiv: Equiv[Map[String, String]] = defaultEquiv
implicit def arrEquiv[T](implicit t: Equiv[T]): Equiv[Array[T]] =
wrapEquiv((x: Array[T]) => x: Seq[T])(seqEquiv[T](t))
implicit def seqEquiv[T](implicit t: Equiv[T]): Equiv[Seq[T]] =
new Equiv[Seq[T]] {
def equiv(a: Seq[T], b: Seq[T]) =
a.length == b.length &&
((a, b).zipped forall t.equiv)
}
def wrapIn[I, J](implicit f: I => J, g: J => I, jCache: SingletonCache[J]): SingletonCache[I] =
new SingletonCache[I] {
override def read(from: Input): I = g(jCache.read(from))
override def write(to: Output, value: I) = jCache.write(to, f(value))
override def equiv: Equiv[I] = wrapEquiv(f)(jCache.equiv)
}
def singleton[T](t: T): SingletonCache[T] =
SingletonCache.basicSingletonCache(asSingleton(t), trueEquiv)
def trueEquiv[T] = new Equiv[T] { def equiv(a: T, b: T) = true }
}

View File

@ -3,247 +3,72 @@
*/
package sbt.internal.util
import sbinary.{ CollectionTypes, DefaultProtocol, Format, Input, JavaFormats, Output => Out }
import java.io.{ ByteArrayInputStream, ByteArrayOutputStream, File, InputStream, OutputStream }
import java.net.{ URI, URL }
import Types.:+:
import DefaultProtocol.{ asSingleton, BooleanFormat, ByteFormat, IntFormat, wrap }
import scala.xml.NodeSeq
import scala.language.existentials
/** The result of a cache query */
sealed trait CacheResult[K]
/** A successful hit on the cache */
case class Hit[O](value: O) extends CacheResult[O]
/**
* A cache miss.
* `update` associates the missing key with `O` in the cache.
*/
case class Miss[O](update: O => Unit) extends CacheResult[O]
/**
* A simple cache with keys of type `I` and values of type `O`
*/
trait Cache[I, O] {
def apply(file: File)(i: I): Either[O, O => Unit]
/**
* Queries the cache backed with store `store` for key `key`.
*/
def apply(store: CacheStore)(key: I): CacheResult[O]
}
trait SBinaryFormats extends CollectionTypes with JavaFormats {
implicit def urlFormat: Format[URL] = DefaultProtocol.UrlFormat
implicit def uriFormat: Format[URI] = DefaultProtocol.UriFormat
}
object Cache extends CacheImplicits {
object Cache {
/**
* Materializes a cache.
*/
def cache[I, O](implicit c: Cache[I, O]): Cache[I, O] = c
def cached[I, O](file: File)(f: I => O)(implicit cache: Cache[I, O]): I => O =
in =>
cache(file)(in) match {
case Left(value) => value
case Right(store) =>
val out = f(in)
store(out)
out
/**
* Returns a function that represents a cache that inserts on miss.
*
* @param store The store that backs this cache.
* @param default A function that computes a default value to insert on
*/
def cached[I, O](store: CacheStore)(default: I => O)(implicit cache: Cache[I, O]): I => O =
key =>
cache(store)(key) match {
case Hit(value) =>
value
case Miss(update) =>
val result = default(key)
update(result)
result
}
def debug[I](label: String, c: InputCache[I]): InputCache[I] =
new InputCache[I] {
type Internal = c.Internal
def convert(i: I) = c.convert(i)
def read(from: Input) =
{
val v = c.read(from)
println(label + ".read: " + v)
v
}
def write(to: Out, v: Internal): Unit = {
println(label + ".write: " + v)
c.write(to, v)
def debug[I](label: String, cache: SingletonCache[I]): SingletonCache[I] =
new SingletonCache[I] {
override def read(from: Input): I = {
val value = cache.read(from)
println(label + ".read: " + value)
value
}
def equiv: Equiv[Internal] = new Equiv[Internal] {
def equiv(a: Internal, b: Internal) =
{
val equ = c.equiv.equiv(a, b)
println(label + ".equiv(" + a + ", " + b + "): " + equ)
equ
}
override def write(to: Output, value: I): Unit = {
println(label + ".write: " + value)
cache.write(to, value)
}
override def equiv: Equiv[I] = new Equiv[I] {
def equiv(a: I, b: I) = {
val equ = cache.equiv.equiv(a, b)
println(label + ".equiv(" + a + ", " + b + "): " + equ)
equ
}
}
}
}
trait CacheImplicits extends BasicCacheImplicits with SBinaryFormats with HListCacheImplicits with UnionImplicits
trait BasicCacheImplicits {
implicit def basicCache[I, O](implicit in: InputCache[I], outFormat: Format[O]): Cache[I, O] =
new BasicCache()(in, outFormat)
def basicInput[I](implicit eq: Equiv[I], fmt: Format[I]): InputCache[I] = InputCache.basicInputCache(fmt, eq)
def defaultEquiv[T]: Equiv[T] = new Equiv[T] { def equiv(a: T, b: T) = a == b }
implicit def optInputCache[T](implicit t: InputCache[T]): InputCache[Option[T]] =
new InputCache[Option[T]] {
type Internal = Option[t.Internal]
def convert(v: Option[T]): Internal = v.map(x => t.convert(x))
def read(from: Input) =
{
val isDefined = BooleanFormat.reads(from)
if (isDefined) Some(t.read(from)) else None
}
def write(to: Out, j: Internal): Unit =
{
BooleanFormat.writes(to, j.isDefined)
j foreach { x => t.write(to, x) }
}
def equiv = optEquiv(t.equiv)
}
def wrapEquiv[S, T](f: S => T)(implicit eqT: Equiv[T]): Equiv[S] =
new Equiv[S] {
def equiv(a: S, b: S) =
eqT.equiv(f(a), f(b))
}
implicit def optEquiv[T](implicit t: Equiv[T]): Equiv[Option[T]] =
new Equiv[Option[T]] {
def equiv(a: Option[T], b: Option[T]) =
(a, b) match {
case (None, None) => true
case (Some(va), Some(vb)) => t.equiv(va, vb)
case _ => false
}
}
implicit def urlEquiv(implicit uriEq: Equiv[URI]): Equiv[URL] = wrapEquiv[URL, URI](_.toURI)(uriEq)
implicit def uriEquiv: Equiv[URI] = defaultEquiv
implicit def stringSetEquiv: Equiv[Set[String]] = defaultEquiv
implicit def stringMapEquiv: Equiv[Map[String, String]] = defaultEquiv
def streamFormat[T](write: (T, OutputStream) => Unit, f: InputStream => T): Format[T] =
{
val toBytes = (t: T) => { val bos = new ByteArrayOutputStream; write(t, bos); bos.toByteArray }
val fromBytes = (bs: Array[Byte]) => f(new ByteArrayInputStream(bs))
wrap(toBytes, fromBytes)(DefaultProtocol.ByteArrayFormat)
}
implicit def xmlInputCache(implicit strEq: InputCache[String]): InputCache[NodeSeq] = wrapIn[NodeSeq, String](_.toString, strEq)
implicit def seqCache[T](implicit t: InputCache[T]): InputCache[Seq[T]] =
new InputCache[Seq[T]] {
type Internal = Seq[t.Internal]
def convert(v: Seq[T]) = v.map(x => t.convert(x))
def read(from: Input) =
{
val size = IntFormat.reads(from)
def next(left: Int, acc: List[t.Internal]): Internal =
if (left <= 0) acc.reverse else next(left - 1, t.read(from) :: acc)
next(size, Nil)
}
def write(to: Out, vs: Internal): Unit = {
val size = vs.length
IntFormat.writes(to, size)
for (v <- vs) t.write(to, v)
}
def equiv: Equiv[Internal] = seqEquiv(t.equiv)
}
implicit def arrEquiv[T](implicit t: Equiv[T]): Equiv[Array[T]] =
wrapEquiv((x: Array[T]) => x: Seq[T])(seqEquiv[T](t))
implicit def seqEquiv[T](implicit t: Equiv[T]): Equiv[Seq[T]] =
new Equiv[Seq[T]] {
def equiv(a: Seq[T], b: Seq[T]) =
a.length == b.length &&
((a, b).zipped forall t.equiv)
}
implicit def seqFormat[T](implicit t: Format[T]): Format[Seq[T]] =
wrap[Seq[T], List[T]](_.toList, _.toSeq)(DefaultProtocol.listFormat)
def wrapIn[I, J](implicit f: I => J, jCache: InputCache[J]): InputCache[I] =
new InputCache[I] {
type Internal = jCache.Internal
def convert(i: I) = jCache.convert(f(i))
def read(from: Input) = jCache.read(from)
def write(to: Out, j: Internal) = jCache.write(to, j)
def equiv = jCache.equiv
}
def singleton[T](t: T): InputCache[T] =
basicInput(trueEquiv, asSingleton(t))
def trueEquiv[T] = new Equiv[T] { def equiv(a: T, b: T) = true }
}
trait HListCacheImplicits {
implicit def hConsCache[H, T <: HList](implicit head: InputCache[H], tail: InputCache[T]): InputCache[H :+: T] =
new InputCache[H :+: T] {
type Internal = (head.Internal, tail.Internal)
def convert(in: H :+: T) = (head.convert(in.head), tail.convert(in.tail))
def read(from: Input) =
{
val h = head.read(from)
val t = tail.read(from)
(h, t)
}
def write(to: Out, j: Internal): Unit = {
head.write(to, j._1)
tail.write(to, j._2)
}
def equiv = new Equiv[Internal] {
def equiv(a: Internal, b: Internal) =
head.equiv.equiv(a._1, b._1) &&
tail.equiv.equiv(a._2, b._2)
}
}
implicit def hNilCache: InputCache[HNil] = Cache.singleton(HNil: HNil)
implicit def hConsFormat[H, T <: HList](implicit head: Format[H], tail: Format[T]): Format[H :+: T] = new Format[H :+: T] {
def reads(from: Input) =
{
val h = head.reads(from)
val t = tail.reads(from)
HCons(h, t)
}
def writes(to: Out, hc: H :+: T): Unit = {
head.writes(to, hc.head)
tail.writes(to, hc.tail)
}
}
implicit def hNilFormat: Format[HNil] = asSingleton(HNil)
}
trait UnionImplicits {
def unionInputCache[UB, HL <: HList](implicit uc: UnionCache[HL, UB]): InputCache[UB] =
new InputCache[UB] {
type Internal = Found[_]
def convert(in: UB) = uc.find(in)
def read(in: Input) =
{
val index = ByteFormat.reads(in).toInt
val (cache, clazz) = uc.at(index)
val value = cache.read(in)
new Found[cache.Internal](cache, clazz, value, index)
}
def write(to: Out, i: Internal): Unit = {
def write0[I](f: Found[I]): Unit = {
ByteFormat.writes(to, f.index.toByte)
f.cache.write(to, f.value)
}
write0(i)
}
def equiv: Equiv[Internal] = new Equiv[Internal] {
def equiv(a: Internal, b: Internal): Boolean =
{
if (a.clazz == b.clazz)
force(a.cache.equiv, a.value, b.value)
else
false
}
def force[T <: UB2, UB2](e: Equiv[T], a: UB2, b: UB2): Boolean = e.equiv(a.asInstanceOf[T], b.asInstanceOf[T])
}
}
implicit def unionCons[H <: UB, UB, T <: HList](implicit head: InputCache[H], mf: Manifest[H], t: UnionCache[T, UB]): UnionCache[H :+: T, UB] =
new UnionCache[H :+: T, UB] {
val size = 1 + t.size
def c = mf.runtimeClass
def find(value: UB): Found[_] =
if (c.isInstance(value)) new Found[head.Internal](head, c, head.convert(value.asInstanceOf[H]), size - 1) else t.find(value)
def at(i: Int): (InputCache[_ <: UB], Class[_]) = if (size == i + 1) (head, c) else t.at(i)
}
implicit def unionNil[UB]: UnionCache[HNil, UB] = new UnionCache[HNil, UB] {
def size = 0
def find(value: UB) = sys.error("No valid sum type for " + value)
def at(i: Int) = sys.error("Invalid union index " + i)
}
final class Found[I](val cache: InputCache[_] { type Internal = I }, val clazz: Class[_], val value: I, val index: Int)
sealed trait UnionCache[HL <: HList, UB] {
def size: Int
def at(i: Int): (InputCache[_ <: UB], Class[_])
def find(forValue: UB): Found[_]
}
}

View File

@ -1,45 +0,0 @@
/* sbt -- Simple Build Tool
* Copyright 2009 Mark Harrah
*/
package sbt.internal.util
import java.io.File
import sbinary.{ DefaultProtocol, Format, Operations }
import scala.reflect.Manifest
import sbt.io.IO
object CacheIO {
def toBytes[T](format: Format[T])(value: T)(implicit mf: Manifest[Format[T]]): Array[Byte] =
toBytes[T](value)(format, mf)
def toBytes[T](value: T)(implicit format: Format[T], mf: Manifest[Format[T]]): Array[Byte] =
Operations.toByteArray(value)(stampedFormat(format))
def fromBytes[T](format: Format[T], default: => T)(bytes: Array[Byte])(implicit mf: Manifest[Format[T]]): T =
fromBytes(default)(bytes)(format, mf)
def fromBytes[T](default: => T)(bytes: Array[Byte])(implicit format: Format[T], mf: Manifest[Format[T]]): T =
if (bytes.isEmpty) default else Operations.fromByteArray(bytes)(stampedFormat(format))
def fromFile[T](format: Format[T], default: => T)(file: File)(implicit mf: Manifest[Format[T]]): T =
fromFile(file, default)(format, mf)
def fromFile[T](file: File, default: => T)(implicit format: Format[T], mf: Manifest[Format[T]]): T =
fromFile[T](file) getOrElse default
def fromFile[T](file: File)(implicit format: Format[T], mf: Manifest[Format[T]]): Option[T] =
try { Some(Operations.fromFile(file)(stampedFormat(format))) }
catch { case e: Exception => None }
def toFile[T](format: Format[T])(value: T)(file: File)(implicit mf: Manifest[Format[T]]): Unit =
toFile(value)(file)(format, mf)
def toFile[T](value: T)(file: File)(implicit format: Format[T], mf: Manifest[Format[T]]): Unit =
{
IO.createDirectory(file.getParentFile)
Operations.toFile(value)(file)(stampedFormat(format))
}
def stampedFormat[T](format: Format[T])(implicit mf: Manifest[Format[T]]): Format[T] =
{
import DefaultProtocol._
withStamp(stamp(format))(format)
}
def stamp[T](format: Format[T])(implicit mf: Manifest[Format[T]]): Int = typeHash(mf)
def typeHash[T](implicit mf: Manifest[T]) = mf.toString.hashCode
def manifest[T](implicit mf: Manifest[T]): Manifest[T] = mf
def objManifest[T](t: T)(implicit mf: Manifest[T]): Manifest[T] = mf
}

View File

@ -0,0 +1,17 @@
package sbt.internal.util
import sbt.datatype.{ ArrayFormat, BooleanFormat, ByteFormat, IntFormat, LongFormat, StringFormat }
import sjsonnew.{ CollectionFormats, TupleFormats }
object CacheImplicits extends BasicCacheImplicits
with ArrayFormat
with BooleanFormat
with ByteFormat
with FileFormat
with IntFormat
with LongFormat
with StringFormat
with URIFormat
with URLFormat
with TupleFormats
with CollectionFormats

View File

@ -0,0 +1,93 @@
package sbt.internal.util
import sjsonnew.{ IsoString, JsonReader, JsonWriter, SupportConverter }
import java.io.{ File, InputStream, OutputStream }
import sbt.io.{ IO, Using }
import sbt.io.syntax.fileToRichFile
/**
* A `CacheStore` is used by the caching infrastructure to persist cached information.
*/
trait CacheStore extends Input with Output {
/** Delete the persisted information. */
def delete(): Unit
}
/**
* Factory that can derive new stores.
*/
trait CacheStoreFactory {
/** Create a new store. */
def derive(identifier: String): CacheStore
}
/**
* A factory that creates new stores persisted in `base`.
*/
class DirectoryStoreFactory[J: IsoString](base: File, converter: SupportConverter[J]) extends CacheStoreFactory {
IO.createDirectory(base)
override def derive(identifier: String): CacheStore =
new FileBasedStore(base / identifier, converter)
}
/**
* A `CacheStore` that persists information in `file`.
*/
class FileBasedStore[J: IsoString](file: File, converter: SupportConverter[J]) extends CacheStore {
IO.touch(file, setModified = false)
override def delete(): Unit =
IO.delete(file)
override def read[T: JsonReader](): T =
Using.fileInputStream(file) { stream =>
val input = new PlainInput(stream, converter)
input.read()
}
override def read[T: JsonReader](default: => T): T =
try read[T]()
catch { case _: Exception => default }
override def write[T: JsonWriter](value: T): Unit =
Using.fileOutputStream(append = false)(file) { stream =>
val output = new PlainOutput(stream, converter)
output.write(value)
}
override def close(): Unit = ()
}
/**
* A store that reads from `inputStream` and writes to `outputStream
*/
class StreamBasedStore[J: IsoString](inputStream: InputStream, outputStream: OutputStream, converter: SupportConverter[J]) extends CacheStore {
override def delete(): Unit = ()
override def read[T: JsonReader](): T = {
val input = new PlainInput(inputStream, converter)
input.read()
}
override def read[T: JsonReader](default: => T): T =
try read[T]()
catch { case _: Exception => default }
override def write[T: JsonWriter](value: T): Unit = {
val output = new PlainOutput(outputStream, converter)
output.write(value)
}
override def close(): Unit = {
inputStream.close()
outputStream.close()
}
}

View File

@ -3,135 +3,172 @@
*/
package sbt.internal.util
import java.io.File
import sbinary.{ DefaultProtocol, Format }
import DefaultProtocol._
import scala.reflect.Manifest
import sbt.io.Hash
import sbt.serialization._
import java.io.File
import sjsonnew.{ Builder, deserializationError, JsonFormat, Unbuilder }
import CacheImplicits._
sealed trait FileInfo {
val file: File
def file: File
}
@directSubclasses(Array(classOf[FileHash], classOf[HashModifiedFileInfo]))
sealed trait HashFileInfo extends FileInfo {
val hash: List[Byte]
def hash: List[Byte]
}
object HashFileInfo {
implicit val pickler: Pickler[HashFileInfo] with Unpickler[HashFileInfo] = PicklerUnpickler.generate[HashFileInfo]
}
@directSubclasses(Array(classOf[FileModified], classOf[HashModifiedFileInfo]))
sealed trait ModifiedFileInfo extends FileInfo {
val lastModified: Long
def lastModified: Long
}
object ModifiedFileInfo {
implicit val pickler: Pickler[ModifiedFileInfo] with Unpickler[ModifiedFileInfo] = PicklerUnpickler.generate[ModifiedFileInfo]
}
@directSubclasses(Array(classOf[PlainFile]))
sealed trait PlainFileInfo extends FileInfo {
def exists: Boolean
}
object PlainFileInfo {
implicit val pickler: Pickler[PlainFileInfo] with Unpickler[PlainFileInfo] = PicklerUnpickler.generate[PlainFileInfo]
}
@directSubclasses(Array(classOf[FileHashModified]))
sealed trait HashModifiedFileInfo extends HashFileInfo with ModifiedFileInfo
object HashModifiedFileInfo {
implicit val pickler: Pickler[HashModifiedFileInfo] with Unpickler[HashModifiedFileInfo] = PicklerUnpickler.generate[HashModifiedFileInfo]
}
private[sbt] final case class PlainFile(file: File, exists: Boolean) extends PlainFileInfo
private[sbt] object PlainFile {
implicit val pickler: Pickler[PlainFile] with Unpickler[PlainFile] = PicklerUnpickler.generate[PlainFile]
}
private[sbt] final case class FileHash(file: File, hash: List[Byte]) extends HashFileInfo
private[sbt] object FileHash {
implicit val pickler: Pickler[FileHash] with Unpickler[FileHash] = PicklerUnpickler.generate[FileHash]
}
private[sbt] final case class FileModified(file: File, lastModified: Long) extends ModifiedFileInfo
private[sbt] object FileModified {
implicit val pickler: Pickler[FileModified] with Unpickler[FileModified] = PicklerUnpickler.generate[FileModified]
}
private[sbt] final case class FileHashModified(file: File, hash: List[Byte], lastModified: Long) extends HashModifiedFileInfo
private[sbt] object FileHashModified {
implicit val pickler: Pickler[FileHashModified] with Unpickler[FileHashModified] = PicklerUnpickler.generate[FileHashModified]
}
sealed trait HashModifiedFileInfo extends HashFileInfo with ModifiedFileInfo
private final case class PlainFile(file: File, exists: Boolean) extends PlainFileInfo
private final case class FileModified(file: File, lastModified: Long) extends ModifiedFileInfo
private final case class FileHash(file: File, hash: List[Byte]) extends HashFileInfo
private final case class FileHashModified(file: File, hash: List[Byte], lastModified: Long) extends HashModifiedFileInfo
object FileInfo {
implicit def existsInputCache: InputCache[PlainFileInfo] = exists.infoInputCache
implicit def modifiedInputCache: InputCache[ModifiedFileInfo] = lastModified.infoInputCache
implicit def hashInputCache: InputCache[HashFileInfo] = hash.infoInputCache
implicit def fullInputCache: InputCache[HashModifiedFileInfo] = full.infoInputCache
implicit val pickler: Pickler[FileInfo] with Unpickler[FileInfo] = PicklerUnpickler.generate[FileInfo]
sealed trait Style {
type F <: FileInfo
implicit def apply(file: File): F
implicit def unapply(info: F): File = info.file
implicit val format: Format[F]
import Cache._
implicit def fileInfoEquiv: Equiv[F] = defaultEquiv
def infoInputCache: InputCache[F] = basicInput
implicit def fileInputCache: InputCache[File] = wrapIn[File, F]
implicit val format: JsonFormat[F]
def apply(file: File): F
def apply(files: Set[File]): FilesInfo[F] = FilesInfo(files map apply)
def unapply(info: F): File = info.file
def unapply(infos: FilesInfo[F]): Set[File] = infos.files map (_.file)
}
object full extends Style {
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), (make _).tupled)
override type F = HashModifiedFileInfo
override implicit val format: JsonFormat[HashModifiedFileInfo] = new JsonFormat[HashModifiedFileInfo] {
override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): HashModifiedFileInfo =
jsOpt match {
case Some(js) =>
unbuilder.beginObject(js)
val file = unbuilder.readField[File]("file")
val hash = unbuilder.readField[List[Byte]]("hash")
val lastModified = unbuilder.readField[Long]("lastModified")
unbuilder.endObject()
FileHashModified(file, hash, lastModified)
case None =>
deserializationError("Expected JsObject but found None")
}
override def write[J](obj: HashModifiedFileInfo, builder: Builder[J]): Unit = {
builder.beginObject()
builder.addField("file", obj.file)
builder.addField("hash", obj.hash)
builder.addField("lastModified", obj.lastModified)
builder.endObject()
}
}
override implicit def apply(file: File): HashModifiedFileInfo =
FileHashModified(file.getAbsoluteFile, Hash(file).toList, file.lastModified)
}
object hash extends Style {
type F = HashFileInfo
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), (make _).tupled)
private def computeHash(file: File): List[Byte] = try { Hash(file).toList } catch { case e: Exception => Nil }
override type F = HashFileInfo
override implicit val format: JsonFormat[HashFileInfo] = new JsonFormat[HashFileInfo] {
override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): HashFileInfo =
jsOpt match {
case Some(js) =>
unbuilder.beginObject(js)
val file = unbuilder.readField[File]("file")
val hash = unbuilder.readField[List[Byte]]("hash")
unbuilder.endObject()
FileHash(file, hash)
case None =>
deserializationError("Expected JsObject but found None")
}
override def write[J](obj: HashFileInfo, builder: Builder[J]): Unit = {
builder.beginObject()
builder.addField("file", obj.file)
builder.addField("hash", obj.hash)
builder.endObject()
}
}
override implicit def apply(file: File): HashFileInfo =
FileHash(file.getAbsoluteFile, computeHash(file))
private def computeHash(file: File): List[Byte] =
try Hash(file).toList
catch { case _: 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), (make _).tupled)
override type F = ModifiedFileInfo
override implicit val format: JsonFormat[ModifiedFileInfo] = new JsonFormat[ModifiedFileInfo] {
override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): ModifiedFileInfo =
jsOpt match {
case Some(js) =>
unbuilder.beginObject(js)
val file = unbuilder.readField[File]("file")
val lastModified = unbuilder.readField[Long]("lastModified")
unbuilder.endObject()
FileModified(file, lastModified)
case None =>
deserializationError("Expected JsObject but found None")
}
override def write[J](obj: ModifiedFileInfo, builder: Builder[J]): Unit = {
builder.beginObject()
builder.addField("file", obj.file)
builder.addField("lastModified", obj.lastModified)
builder.endObject()
}
}
override implicit def apply(file: File): ModifiedFileInfo =
FileModified(file.getAbsoluteFile, file.lastModified)
}
object exists extends Style {
type F = PlainFileInfo
implicit def apply(file: File): PlainFileInfo = make(file)
def make(file: File): PlainFileInfo = { val abs = file.getAbsoluteFile; PlainFile(abs, abs.exists) }
implicit val format: Format[PlainFileInfo] = asProduct2[PlainFileInfo, File, Boolean](PlainFile.apply)(x => (x.file, x.exists))
override type F = PlainFileInfo
override implicit val format: JsonFormat[PlainFileInfo] = new JsonFormat[PlainFileInfo] {
override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): PlainFileInfo =
jsOpt match {
case Some(js) =>
unbuilder.beginObject(js)
val file = unbuilder.readField[File]("file")
val exists = unbuilder.readField[Boolean]("exists")
unbuilder.endObject()
PlainFile(file, exists)
case None =>
deserializationError("Expected JsObject but found None")
}
override def write[J](obj: PlainFileInfo, builder: Builder[J]): Unit = {
builder.beginObject()
builder.addField("file", obj.file)
builder.addField("exists", obj.exists)
builder.endObject()
}
}
override implicit def apply(file: File): PlainFileInfo = {
val abs = file.getAbsoluteFile
PlainFile(abs, abs.exists)
}
}
}
final case class FilesInfo[F <: FileInfo] private (files: Set[F])
object FilesInfo {
sealed abstract class Style {
type F <: FileInfo
val fileStyle: FileInfo.Style { type F = Style.this.F }
//def manifest: Manifest[F] = fileStyle.manifest
implicit def apply(files: Set[File]): FilesInfo[F]
implicit def unapply(info: FilesInfo[F]): Set[File] = info.files.map(_.file)
implicit val formats: Format[FilesInfo[F]]
val manifest: Manifest[Format[FilesInfo[F]]]
def empty: FilesInfo[F] = new FilesInfo[F](Set.empty)
import Cache._
def infosInputCache: InputCache[FilesInfo[F]] = basicInput
implicit def filesInputCache: InputCache[Set[File]] = wrapIn[Set[File], FilesInfo[F]]
implicit def filesInfoEquiv: Equiv[FilesInfo[F]] = defaultEquiv
}
private final class BasicStyle[FI <: FileInfo](style: FileInfo.Style { type F = FI })(implicit val manifest: Manifest[Format[FilesInfo[FI]]]) extends Style {
type F = FI
val fileStyle: FileInfo.Style { type F = FI } = style
private implicit val infoFormat: Format[FI] = fileStyle.format
implicit def apply(files: Set[File]): FilesInfo[F] = FilesInfo(files.map(_.getAbsoluteFile).map(fileStyle.apply))
implicit val formats: Format[FilesInfo[F]] = wrap(_.files, (fs: Set[F]) => new FilesInfo(fs))
}
lazy val full: Style { type F = HashModifiedFileInfo } = new BasicStyle(FileInfo.full)
lazy val hash: Style { type F = HashFileInfo } = new BasicStyle(FileInfo.hash)
lazy val lastModified: Style { type F = ModifiedFileInfo } = new BasicStyle(FileInfo.lastModified)
lazy val exists: Style { type F = PlainFileInfo } = new BasicStyle(FileInfo.exists)
implicit def existsInputsCache: InputCache[FilesInfo[PlainFileInfo]] = exists.infosInputCache
implicit def hashInputsCache: InputCache[FilesInfo[HashFileInfo]] = hash.infosInputCache
implicit def modifiedInputsCache: InputCache[FilesInfo[ModifiedFileInfo]] = lastModified.infosInputCache
implicit def fullInputsCache: InputCache[FilesInfo[HashModifiedFileInfo]] = full.infosInputCache
implicit def format[F <: FileInfo]: JsonFormat[FilesInfo[F]] = implicitly
def empty[F <: FileInfo]: FilesInfo[F] = FilesInfo(Set.empty[F])
}

View File

@ -0,0 +1,45 @@
package sbt.internal.util
import sbt.io.{ IO, Using }
import java.io.{ Closeable, InputStream }
import scala.util.{ Failure, Success }
import sjsonnew.{ IsoString, JsonReader, SupportConverter }
trait Input extends Closeable {
def read[T: JsonReader](): T
def read[T: JsonReader](default: => T): T
}
class PlainInput[J: IsoString](input: InputStream, converter: SupportConverter[J]) extends Input {
val isoFormat: IsoString[J] = implicitly
private def readFully(): String = {
Using.streamReader(input, IO.utf8) { reader =>
val builder = new StringBuilder()
val bufferSize = 1024
val buffer = new Array[Char](bufferSize)
var read = 0
while ({ read = reader.read(buffer, 0, bufferSize); read != -1 }) {
builder.append(String.valueOf(buffer.take(read)))
}
builder.toString()
}
}
override def read[T: JsonReader](): T = {
val string = readFully()
val json = isoFormat.from(string)
converter.fromJson(json) match {
case Success(value) => value
case Failure(ex) => throw ex
}
}
override def read[T: JsonReader](default: => T): T =
try read[T]()
catch { case _: Exception => default }
override def close(): Unit = input.close()
}

View File

@ -0,0 +1,32 @@
package sbt.internal.util
import sbt.io.Using
import java.io.{ Closeable, OutputStream }
import scala.util.{ Failure, Success }
import sjsonnew.{ IsoString, JsonWriter, SupportConverter }
trait Output extends Closeable {
def write[T: JsonWriter](value: T): Unit
}
class PlainOutput[J: IsoString](output: OutputStream, converter: SupportConverter[J]) extends Output {
val isoFormat: IsoString[J] = implicitly
override def write[T: JsonWriter](value: T): Unit = {
converter.toJson(value) match {
case Success(js) =>
val asString = isoFormat.to(js)
Using.bufferedOutputStream(output) { writer =>
val out = new java.io.PrintWriter(writer)
out.print(asString)
out.flush()
}
case Failure(ex) =>
throw ex
}
}
override def close(): Unit = output.close()
}

View File

@ -3,59 +3,61 @@
*/
package sbt.internal.util
import sbinary.{ Format, Input, Output => Out }
import java.io.File
import sbt.io.Using
import scala.util.Try
trait InputCache[I] {
type Internal
def convert(i: I): Internal
def read(from: Input): Internal
def write(to: Out, j: Internal): Unit
def equiv: Equiv[Internal]
import sjsonnew.JsonFormat
import CacheImplicits._
/**
* A cache that stores a single value.
*/
trait SingletonCache[T] {
/** Reads the cache from the backing `from`. */
def read(from: Input): T
/** Writes `value` to the backing `to`. */
def write(to: Output, value: T): Unit
/** Equivalence for elements of type `T`. */
def equiv: Equiv[T]
}
object InputCache {
implicit def basicInputCache[I](implicit fmt: Format[I], eqv: Equiv[I]): InputCache[I] =
new InputCache[I] {
type Internal = I
def convert(i: I) = i
def read(from: Input): I = fmt.reads(from)
def write(to: Out, i: I) = fmt.writes(to, i)
def equiv = eqv
object SingletonCache {
implicit def basicSingletonCache[T: JsonFormat: Equiv]: SingletonCache[T] =
new SingletonCache[T] {
override def read(from: Input): T = from.read[T]
override def write(to: Output, value: T) = to.write(value)
override def equiv: Equiv[T] = implicitly
}
def lzy[I](mkIn: => InputCache[I]): InputCache[I] =
new InputCache[I] {
lazy val ic = mkIn
type Internal = ic.Internal
def convert(i: I) = ic convert i
def read(from: Input): ic.Internal = ic.read(from)
def write(to: Out, i: ic.Internal) = ic.write(to, i)
def equiv = ic.equiv
/** A lazy `SingletonCache` */
def lzy[T: JsonFormat: Equiv](mkCache: => SingletonCache[T]): SingletonCache[T] =
new SingletonCache[T] {
lazy val cache = mkCache
override def read(from: Input): T = cache.read(from)
override def write(to: Output, value: T) = cache.write(to, value)
override def equiv = cache.equiv
}
}
class BasicCache[I, O](implicit input: InputCache[I], outFormat: Format[O]) extends Cache[I, O] {
def apply(file: File)(in: I) =
{
val j = input.convert(in)
try { applyImpl(file, j) }
catch { case e: Exception => Right(update(file)(j)) }
}
protected def applyImpl(file: File, in: input.Internal) =
{
Using.fileInputStream(file) { stream =>
val previousIn = input.read(stream)
if (input.equiv.equiv(in, previousIn))
Left(outFormat.reads(stream))
else
Right(update(file)(in))
}
}
protected def update(file: File)(in: input.Internal) = (out: O) =>
{
Using.fileOutputStream(false)(file) { stream =>
input.write(stream, in)
outFormat.writes(stream, out)
}
}
/**
* Simple key-value cache.
*/
class BasicCache[I: JsonFormat: Equiv, O: JsonFormat] extends Cache[I, O] {
private val singletonCache: SingletonCache[(I, O)] = implicitly
val equiv: Equiv[I] = implicitly
override def apply(store: CacheStore)(key: I): CacheResult[O] =
Try {
val (previousKey, previousValue) = singletonCache.read(store)
if (equiv.equiv(key, previousKey))
Hit(previousValue)
else
Miss(update(store)(key))
} getOrElse Miss(update(store)(key))
private def update(store: CacheStore)(key: I) = (value: O) => {
singletonCache.write(store, (key, value))
}
}

View File

@ -0,0 +1,44 @@
package sbt.internal.util
import scala.reflect.Manifest
import sbt.datatype.IntFormat
import sjsonnew.{ Builder, deserializationError, JsonFormat, Unbuilder }
object StampedFormat extends IntFormat {
def apply[T](format: JsonFormat[T])(implicit mf: Manifest[JsonFormat[T]]): JsonFormat[T] = {
withStamp(stamp(format))(format)
}
def withStamp[T, S](stamp: S)(format: JsonFormat[T])(implicit formatStamp: JsonFormat[S], equivStamp: Equiv[S]): JsonFormat[T] =
new JsonFormat[T] {
override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): T =
jsOpt match {
case Some(js) =>
unbuilder.extractArray(js) match {
case Vector(readStamp, readValue) =>
val actualStamp = formatStamp.read(Some(readStamp), unbuilder)
if (equivStamp.equiv(actualStamp, stamp)) format.read(Some(readValue), unbuilder)
else sys.error(s"Incorrect stamp. Expected: $stamp, Found: $readStamp")
case other =>
deserializationError(s"Expected JsArray of size 2, but found JsArray of size ${other.size}")
}
case None =>
deserializationError("Expected JsArray but found None.")
}
override def write[J](obj: T, builder: Builder[J]): Unit = {
builder.beginArray()
formatStamp.write(stamp, builder)
format.write(obj, builder)
builder.endArray()
}
}
private def stamp[T](format: JsonFormat[T])(implicit mf: Manifest[JsonFormat[T]]): Int = typeHash(mf)
private def typeHash[T](implicit mf: Manifest[T]) = mf.toString.hashCode
}

View File

@ -0,0 +1,76 @@
package sbt.internal.util
import sbt.io.IO
import sbt.io.syntax._
import CacheImplicits._
import sjsonnew.{ Builder, deserializationError, IsoString, JsonFormat, Unbuilder }
import sjsonnew.support.scalajson.unsafe.{ CompactPrinter, Converter, FixedParser }
import scala.json.ast.unsafe.JValue
class CacheSpec extends UnitSpec {
implicit val isoString: IsoString[JValue] = IsoString.iso(CompactPrinter.apply, FixedParser.parseUnsafe)
"A cache" should "NOT throw an exception if read without being written previously" in {
testCache[String, Int] {
case (cache, store) =>
cache(store)("missing") match {
case Hit(_) => fail
case Miss(_) => ()
}
}
}
it should "write a very simple value" in {
testCache[String, Int] {
case (cache, store) =>
cache(store)("missing") match {
case Hit(_) => fail
case Miss(update) => update(5)
}
}
}
it should "be updatable" in {
testCache[String, Int] {
case (cache, store) =>
val value = 5
cache(store)("someKey") match {
case Hit(_) => fail
case Miss(update) => update(value)
}
cache(store)("someKey") match {
case Hit(read) => assert(read === value)
case Miss(_) => fail
}
}
}
it should "return the value that has been previously written" in {
testCache[String, Int] {
case (cache, store) =>
val key = "someKey"
val value = 5
cache(store)(key) match {
case Hit(_) => fail
case Miss(update) => update(value)
}
cache(store)(key) match {
case Hit(read) => assert(read === value)
case Miss(_) => fail
}
}
}
private def testCache[K, V](f: (Cache[K, V], CacheStore) => Unit)(implicit cache: Cache[K, V]): Unit =
IO.withTemporaryDirectory { tmp =>
val store = new FileBasedStore(tmp / "cache-store", Converter)
f(cache, store)
}
}

View File

@ -1,32 +0,0 @@
package sbt.internal.util
import java.io.File
import Types.:+:
object CacheTest // extends Properties("Cache test")
{
val lengthCache = new File("/tmp/length-cache")
val cCache = new File("/tmp/c-cache")
import Cache._
import FileInfo.hash._
import Ordering._
import sbinary.DefaultProtocol.FileFormat
def test(): Unit = {
lazy val create = new File("test")
val length = cached(lengthCache) {
(f: File) => { println("File length: " + f.length); f.length }
}
lazy val fileLength = length(create)
val c = cached(cCache) { (in: (File :+: Long :+: HNil)) =>
val file :+: len :+: HNil = in
println("File: " + file + " (" + file.exists + "), length: " + len)
(len + 1) :+: file :+: HNil
}
c(create :+: fileLength :+: HNil)
()
}
}

View File

@ -0,0 +1,91 @@
package sbt.internal.util
import sbt.io.IO
import sbt.io.syntax._
import CacheImplicits._
import sjsonnew.{ Builder, deserializationError, IsoString, JsonFormat, Unbuilder }
import sjsonnew.support.scalajson.unsafe.{ CompactPrinter, Converter, FixedParser }
import scala.json.ast.unsafe.JValue
class SingletonCacheSpec extends UnitSpec {
case class ComplexType(val x: Int, y: String, z: List[Int])
object ComplexType {
implicit val format: JsonFormat[ComplexType] =
new JsonFormat[ComplexType] {
override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): ComplexType = {
jsOpt match {
case Some(js) =>
unbuilder.beginObject(js)
val x = unbuilder.readField[Int]("x")
val y = unbuilder.readField[String]("y")
val z = unbuilder.readField[List[Int]]("z")
unbuilder.endObject()
ComplexType(x, y, z)
case None =>
deserializationError("Exception JObject but found None")
}
}
override def write[J](obj: ComplexType, builder: Builder[J]): Unit = {
builder.beginObject()
builder.addField("x", obj.x)
builder.addField("y", obj.y)
builder.addField("z", obj.z)
builder.endObject()
}
}
}
implicit val isoString: IsoString[JValue] = IsoString.iso(CompactPrinter.apply, FixedParser.parseUnsafe)
"A singleton cache" should "throw an exception if read without being written previously" in {
testCache[Int] {
case (cache, store) =>
intercept[Exception] {
cache.read(store)
}
()
}
}
it should "write a very simple value" in {
testCache[Int] {
case (cache, store) =>
cache.write(store, 5)
}
}
it should "return the simple value that has been previously written" in {
testCache[Int] {
case (cache, store) =>
val value = 5
cache.write(store, value)
val read = cache.read(store)
assert(read === value)
}
}
it should "write a complex value" in {
testCache[ComplexType] {
case (cache, store) =>
val value = ComplexType(1, "hello, world!", (1 to 10 by 3).toList)
cache.write(store, value)
val read = cache.read(store)
assert(read === value)
}
}
private def testCache[T](f: (SingletonCache[T], CacheStore) => Unit)(implicit cache: SingletonCache[T]): Unit =
IO.withTemporaryDirectory { tmp =>
val store = new FileBasedStore(tmp / "cache-store", Converter)
f(cache, store)
}
}

View File

@ -0,0 +1,29 @@
package sjsonnew
package support.scalajson.unsafe
import scala.json.ast.unsafe._
import scala.collection.mutable
import jawn.{ SupportParser, MutableFacade }
object FixedParser extends SupportParser[JValue] {
implicit val facade: MutableFacade[JValue] =
new MutableFacade[JValue] {
def jnull() = JNull
def jfalse() = JTrue
def jtrue() = JFalse
def jnum(s: String) = JNumber(s)
def jint(s: String) = JNumber(s)
def jstring(s: String) = JString(s)
def jarray(vs: mutable.ArrayBuffer[JValue]) = JArray(vs.toArray)
def jobject(vs: mutable.Map[String, JValue]) = {
val array = new Array[JField](vs.size)
var i = 0
vs.foreach {
case (key, value) =>
array(i) = JField(key, value)
i += 1
}
JObject(array)
}
}
}

View File

@ -3,133 +3,76 @@
*/
package sbt.internal.util
import java.io.{ File, IOException }
import CacheIO.{ fromFile, toFile }
import sbinary.Format
import scala.pickling.PicklingException
import scala.reflect.Manifest
import sbt.io.IO.{ delete, read, write }
import scala.util.{ Failure, Try, Success }
import java.io.File
import sbt.io.IO
import sbt.io.Using
import sbt.io.syntax._
import sbt.serialization._
import sjsonnew.JsonFormat
object Tracked {
import CacheImplicits.LongFormat
/**
* Creates a tracker that provides the last time it was evaluated.
* 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 = true): Timestamp = new Timestamp(cacheFile, useStartTime)
/** Creates a tracker that only evaluates a function when the input has changed.*/
//def changed[O](cacheFile: File)(implicit format: Format[O], equiv: Equiv[O]): Changed[O] =
// new Changed[O](cacheFile)
def tstamp(store: CacheStore, useStartTime: Boolean = true): Timestamp = new Timestamp(store, useStartTime)
/** Creates a tracker that provides the difference between a set of input files for successive invocations.*/
def diffInputs(cache: File, style: FilesInfo.Style): Difference =
Difference.inputs(cache, style)
def diffInputs(store: CacheStore, style: FileInfo.Style): Difference =
Difference.inputs(store, style)
/** Creates a tracker that provides the difference between a set of output files for successive invocations.*/
def diffOutputs(cache: File, style: FilesInfo.Style): Difference =
Difference.outputs(cache, style)
def diffOutputs(store: CacheStore, style: FileInfo.Style): Difference =
Difference.outputs(store, style)
def lastOutput[I, O](cacheFile: File)(f: (I, Option[O]) => O)(implicit o: Format[O], mf: Manifest[Format[O]]): I => O = in =>
{
val previous: Option[O] = fromFile[O](cacheFile)
val next = f(in, previous)
toFile(next)(cacheFile)
next
/** Creates a tracker that provides the output of the most recent invocation of the function */
def lastOutput[I, O: JsonFormat](store: CacheStore)(f: (I, Option[O]) => O): I => O = { in =>
val previous = Try { store.read[O] }.toOption
val next = f(in, previous)
store.write(next)
next
}
/**
* Creates a tracker that indicates whether the arguments given to f have changed since the most
* recent invocation.
*/
def inputChanged[I: JsonFormat: SingletonCache, O](store: CacheStore)(f: (Boolean, I) => O): I => O = { in =>
val cache: SingletonCache[I] = implicitly
val help = new CacheHelp(cache)
val changed = help.changed(store, in)
val result = f(changed, in)
if (changed)
help.save(store, in)
result
}
private final class CacheHelp[I: JsonFormat](val sc: SingletonCache[I]) {
def save(store: CacheStore, value: I): Unit = {
store.write(value)
}
// Todo: This function needs more testing.
private[sbt] def lastOutputWithJson[I, O: Pickler: Unpickler](cacheFile: File)(f: (I, Option[O]) => O): I => O = in =>
{
val previous: Option[O] = try {
fromJsonFile[O](cacheFile).toOption
} catch {
case e: PicklingException => None
case e: IOException => None
def changed(store: CacheStore, value: I): Boolean =
Try { store.read[I] } match {
case Success(prev) => !sc.equiv.equiv(value, prev)
case Failure(_) => true
}
val next = f(in, previous)
IO.createDirectory(cacheFile.getParentFile)
toJsonFile(next, cacheFile)
next
}
def inputChanged[I, O](cacheFile: File)(f: (Boolean, I) => O)(implicit ic: InputCache[I]): I => O = in =>
{
val help = new CacheHelp(ic)
val conv = help.convert(in)
val changed = help.changed(cacheFile, conv)
val result = f(changed, in)
if (changed)
help.save(cacheFile, conv)
result
}
private[sbt] def inputChangedWithJson[I: Pickler: Unpickler, O](cacheFile: File)(f: (Boolean, I) => O): I => O = in =>
{
val help = new JsonCacheHelp[I]
val conv = help.convert(in)
val changed = help.changed(cacheFile, conv)
val result = f(changed, in)
if (changed)
help.save(cacheFile, conv)
result
}
def outputChanged[I, O](cacheFile: File)(f: (Boolean, I) => O)(implicit ic: InputCache[I]): (() => I) => O = in =>
{
val initial = in()
val help = new CacheHelp(ic)
val changed = help.changed(cacheFile, help.convert(initial))
val result = f(changed, initial)
if (changed)
help.save(cacheFile, help.convert(in()))
result
}
private[sbt] def outputChangedWithJson[I: Pickler, O](cacheFile: File)(f: (Boolean, I) => O): (() => I) => O = in =>
{
val initial = in()
val help = new JsonCacheHelp[I]
val changed = help.changed(cacheFile, help.convert(initial))
val result = f(changed, initial)
if (changed)
help.save(cacheFile, help.convert(in()))
result
}
final class CacheHelp[I](val ic: InputCache[I]) {
def convert(i: I): ic.Internal = ic.convert(i)
def save(cacheFile: File, value: ic.Internal): Unit =
Using.fileOutputStream()(cacheFile)(out => ic.write(out, value))
def changed(cacheFile: File, converted: ic.Internal): Boolean =
try {
val prev = Using.fileInputStream(cacheFile)(x => ic.read(x))
!ic.equiv.equiv(converted, prev)
} catch { case e: Exception => true }
}
private[sbt] final class JsonCacheHelp[I: Pickler] {
def convert(i: I): String = toJsonString(i)
def save(cacheFile: File, value: String): Unit =
IO.write(cacheFile, value, IO.utf8)
def changed(cacheFile: File, converted: String): Boolean =
try {
val prev = IO.read(cacheFile, IO.utf8)
converted != prev
} catch { case e: Exception => true }
}
}
trait Tracked {
/** Cleans outputs and clears the cache.*/
def clean(): Unit
}
class Timestamp(val cacheFile: File, useStartTime: Boolean) extends Tracked {
def clean() = delete(cacheFile)
class Timestamp(val store: CacheStore, useStartTime: Boolean)(implicit format: JsonFormat[Long]) extends Tracked {
def clean() = store.delete()
/**
* Reads the previous timestamp, evaluates the provided function,
* and then updates the timestamp if the function completes normally.
@ -138,17 +81,16 @@ class Timestamp(val cacheFile: File, useStartTime: Boolean) extends Tracked {
{
val start = now()
val result = f(readTimestamp)
write(cacheFile, (if (useStartTime) start else now()).toString)
store.write(if (useStartTime) start else now())
result
}
private def now() = System.currentTimeMillis
def readTimestamp: Long =
try { read(cacheFile).toLong }
catch { case _: NumberFormatException | _: java.io.FileNotFoundException => 0 }
Try { store.read[Long] } getOrElse 0
}
class Changed[O](val cacheFile: File)(implicit equiv: Equiv[O], format: Format[O]) extends Tracked {
def clean() = delete(cacheFile)
class Changed[O: Equiv: JsonFormat](val store: CacheStore) extends Tracked {
def clean() = store.delete()
def apply[O2](ifChanged: O => O2, ifUnchanged: O => O2): O => O2 = value =>
{
if (uptodate(value))
@ -159,19 +101,15 @@ class Changed[O](val cacheFile: File)(implicit equiv: Equiv[O], format: Format[O
}
}
def update(value: O): Unit = Using.fileOutputStream(false)(cacheFile)(stream => format.writes(stream, value))
def uptodate(value: O): Boolean =
try {
Using.fileInputStream(cacheFile) {
stream => equiv.equiv(value, format.reads(stream))
}
} catch {
case _: Exception => false
}
def update(value: O): Unit = store.write(value) //Using.fileOutputStream(false)(cacheFile)(stream => format.writes(stream, value))
def uptodate(value: O): Boolean = {
val equiv: Equiv[O] = implicitly
equiv.equiv(value, store.read[O])
}
}
object Difference {
def constructor(defineClean: Boolean, filesAreOutputs: Boolean): (File, FilesInfo.Style) => Difference =
(cache, style) => new Difference(cache, style, defineClean, filesAreOutputs)
def constructor(defineClean: Boolean, filesAreOutputs: Boolean): (CacheStore, FileInfo.Style) => Difference =
(store, style) => new Difference(store, style, defineClean, filesAreOutputs)
/**
* Provides a constructor for a Difference that removes the files from the previous run on a call to 'clean' and saves the
@ -185,15 +123,15 @@ object Difference {
*/
val inputs = constructor(false, false)
}
class Difference(val cache: File, val style: FilesInfo.Style, val defineClean: Boolean, val filesAreOutputs: Boolean) extends Tracked {
class Difference(val store: CacheStore, val style: FileInfo.Style, val defineClean: Boolean, val filesAreOutputs: Boolean) extends Tracked {
def clean() =
{
if (defineClean) delete(raw(cachedFilesInfo)) else ()
if (defineClean) IO.delete(raw(cachedFilesInfo)) else ()
clearCache()
}
private def clearCache() = delete(cache)
private def clearCache() = store.delete()
private def cachedFilesInfo = fromFile(style.formats, style.empty)(cache)(style.manifest).files
private def cachedFilesInfo = store.read(default = FilesInfo.empty[style.F]).files //(style.formats).files
private def raw(fs: Set[style.F]): Set[File] = fs.map(_.file)
def apply[T](files: Set[File])(f: ChangeReport[File] => T): T =
@ -225,7 +163,9 @@ class Difference(val cache: File, val style: FilesInfo.Style, val defineClean: B
val result = f(report)
val info = if (filesAreOutputs) style(abs(extractFiles(result))) else currentFilesInfo
toFile(style.formats)(info)(cache)(style.manifest)
store.write(info)
result
}
}
@ -240,24 +180,24 @@ object FileFunction {
* (which does the actual work: compiles, generates resources, etc.), returning
* a Set of output files that it generated.
*
* The input file and resulting output file state is cached in
* cacheBaseDirectory. On each invocation, the state of the input and output
* The input file and resulting output file state is cached in stores issued by
* `storeFactory`. On each invocation, the state of the input and output
* files from the previous run is compared against the cache, as is the set of
* input files. If a change in file state / input files set is detected, the
* action function is re-executed.
*
* @param cacheBaseDirectory The folder in which to store
* @param storeFactory The factory to use to get stores for the input and output files.
* @param inStyle The strategy by which to detect state change in the input files from the previous run
* @param outStyle The strategy by which to detect state change in the output files from the previous run
* @param action The work function, which receives a list of input files and returns a list of output files
*/
def cached(cacheBaseDirectory: File, inStyle: FilesInfo.Style = FilesInfo.lastModified, outStyle: FilesInfo.Style = FilesInfo.exists)(action: Set[File] => Set[File]): Set[File] => Set[File] =
cached(cacheBaseDirectory)(inStyle, outStyle)((in, out) => action(in.checked))
def cached(storeFactory: CacheStoreFactory, inStyle: FileInfo.Style = FileInfo.lastModified, outStyle: FileInfo.Style = FileInfo.exists)(action: Set[File] => Set[File]): Set[File] => Set[File] =
cached(storeFactory)(inStyle, outStyle)((in, out) => action(in.checked))
def cached(cacheBaseDirectory: File)(inStyle: FilesInfo.Style, outStyle: FilesInfo.Style)(action: UpdateFunction): Set[File] => Set[File] =
def cached(storeFactory: CacheStoreFactory)(inStyle: FileInfo.Style, outStyle: FileInfo.Style)(action: UpdateFunction): Set[File] => Set[File] =
{
lazy val inCache = Difference.inputs(cacheBaseDirectory / "in-cache", inStyle)
lazy val outCache = Difference.outputs(cacheBaseDirectory / "out-cache", outStyle)
lazy val inCache = Difference.inputs(storeFactory.derive("in-cache"), inStyle)
lazy val outCache = Difference.outputs(storeFactory.derive("out-cache"), outStyle)
inputs =>
{
inCache(inputs) { inReport =>

View File

@ -0,0 +1,140 @@
package sbt.internal.util
import sbt.io.IO
import sbt.io.syntax._
import CacheImplicits._
import sjsonnew.{ Builder, deserializationError, IsoString, JsonFormat, Unbuilder }
import sjsonnew.support.scalajson.unsafe.{ CompactPrinter, Converter, FixedParser }
import scala.json.ast.unsafe.JValue
class TrackedSpec extends UnitSpec {
implicit val isoString: IsoString[JValue] = IsoString.iso(CompactPrinter.apply, FixedParser.parseUnsafe)
"lastOutput" should "store the last output" in {
withStore { store =>
val value = 5
val otherValue = 10
val res0 =
Tracked.lastOutput[Int, Int](store) {
case (in, None) =>
assert(in === value)
in
case (in, Some(_)) =>
fail()
}(implicitly)(value)
assert(res0 === value)
val res1 =
Tracked.lastOutput[Int, Int](store) {
case (in, None) =>
fail()
case (in, Some(read)) =>
assert(in === otherValue)
assert(read === value)
read
}(implicitly)(otherValue)
assert(res1 === value)
val res2 =
Tracked.lastOutput[Int, Int](store) {
case (in, None) =>
fail()
case (in, Some(read)) =>
assert(in === otherValue)
assert(read === value)
read
}(implicitly)(otherValue)
assert(res2 === value)
}
}
"inputChanged" should "detect that the input has not changed" in {
withStore { store =>
val input0 = 0
val res0 =
Tracked.inputChanged[Int, Int](store) {
case (true, in) =>
assert(in === input0)
in
case (false, in) =>
fail()
}(implicitly, implicitly)(input0)
assert(res0 === input0)
val res1 =
Tracked.inputChanged[Int, Int](store) {
case (true, in) =>
fail()
case (false, in) =>
assert(in === input0)
in
}(implicitly, implicitly)(input0)
assert(res1 === input0)
}
}
it should "detect that the input has changed" in {
withStore { store =>
val input0 = 0
val input1 = 1
val res0 =
Tracked.inputChanged[Int, Int](store) {
case (true, in) =>
assert(in === input0)
in
case (false, in) =>
fail()
}(implicitly, implicitly)(input0)
assert(res0 === input0)
val res1 =
Tracked.inputChanged[Int, Int](store) {
case (true, in) =>
assert(in === input1)
in
case (false, in) =>
fail()
}(implicitly, implicitly)(input1)
assert(res1 === input1)
}
}
"tstamp tracker" should "have a timestamp of 0 on first invocation" in {
withStore { store =>
Tracked.tstamp(store) { last =>
assert(last === 0)
}
}
}
it should "provide the last time a function has been evaluated" in {
withStore { store =>
Tracked.tstamp(store) { last =>
assert(last === 0)
}
Tracked.tstamp(store) { last =>
val difference = System.currentTimeMillis - last
assert(difference < 1000)
}
}
}
private def withStore(f: CacheStore => Unit): Unit =
IO.withTemporaryDirectory { tmp =>
val store = new FileBasedStore(tmp / "cache-store", Converter)
f(store)
}
}

View File

@ -0,0 +1,29 @@
package sjsonnew
package support.scalajson.unsafe
import scala.json.ast.unsafe._
import scala.collection.mutable
import jawn.{ SupportParser, MutableFacade }
object FixedParser extends SupportParser[JValue] {
implicit val facade: MutableFacade[JValue] =
new MutableFacade[JValue] {
def jnull() = JNull
def jfalse() = JTrue
def jtrue() = JFalse
def jnum(s: String) = JNumber(s)
def jint(s: String) = JNumber(s)
def jstring(s: String) = JString(s)
def jarray(vs: mutable.ArrayBuffer[JValue]) = JArray(vs.toArray)
def jobject(vs: mutable.Map[String, JValue]) = {
val array = new Array[JField](vs.size)
var i = 0
vs.foreach {
case (key, value) =>
array(i) = JField(key, value)
i += 1
}
JObject(array)
}
}
}

View File

@ -8,7 +8,6 @@ object Dependencies {
lazy val sbtIO = "org.scala-sbt" %% "io" % "1.0.0-M6"
lazy val jline = "jline" % "jline" % "2.13"
lazy val sbtSerialization = "org.scala-sbt" %% "serialization" % "0.1.2"
lazy val sbinary = "org.scala-sbt" %% "sbinary" % "0.4.3"
lazy val scalaCompiler = Def.setting { "org.scala-lang" % "scala-compiler" % scalaVersion.value }
lazy val scalaReflect = Def.setting { "org.scala-lang" % "scala-reflect" % scalaVersion.value }
@ -27,4 +26,7 @@ object Dependencies {
val scalatest = "org.scalatest" %% "scalatest" % "2.2.6"
lazy val parserCombinator211 = "org.scala-lang.modules" %% "scala-parser-combinators" % "1.0.4"
lazy val datatypeCodecs = "org.scala-sbt" %% "datatype-codecs" % "1.0.0-SNAPSHOT"
lazy val sjsonnewScalaJson = "com.eed3si9n" %% "sjson-new-scalajson" % "0.4.0"
}