Tests and fixes for component manager and cache interface.

This commit is contained in:
Mark Harrah 2009-08-20 00:02:06 -04:00
parent a70ddd8e32
commit 108807a773
12 changed files with 333 additions and 83 deletions

View File

@ -41,7 +41,9 @@ class Compiler(scalaLoader: ClassLoader, val scalaVersion: String, private[xsbt]
val interfaceLoader = new URLClassLoader(Array(interfaceJar.toURI.toURL), scalaLoader) val interfaceLoader = new URLClassLoader(Array(interfaceJar.toURI.toURL), scalaLoader)
val interface = Class.forName("xsbt.CompilerInterface", true, interfaceLoader).newInstance val interface = Class.forName("xsbt.CompilerInterface", true, interfaceLoader).newInstance
val runnable = interface.asInstanceOf[{ def run(args: Array[String], callback: AnalysisCallback, maximumErrors: Int, log: Logger): Unit }] val runnable = interface.asInstanceOf[{ def run(args: Array[String], callback: AnalysisCallback, maximumErrors: Int, log: Logger): Unit }]
runnable.run(argsWithPlugin.toArray, callback, maximumErrors, log) // safe to pass across the ClassLoader boundary because the types are defined in Java // these arguments are safe to pass across the ClassLoader boundary because the types are defined in Java
// so they will be binary compatible across all versions of Scala
runnable.run(argsWithPlugin.toArray, callback, maximumErrors, log)
} }
def forceInitialization() {interfaceJar } def forceInitialization() {interfaceJar }
} }

View File

@ -11,7 +11,7 @@ object CheckBasic extends Specification
"Compiling basic file should succeed" in { "Compiling basic file should succeed" in {
WithFiles(basicName -> basicSource){ files => WithFiles(basicName -> basicSource){ files =>
TestCompile(files){ loader => Class.forName("org.example.Basic", false, loader) } TestCompile(files){ loader => Class.forName("org.example.Basic", false, loader) }
true must be(true) // don't know how to just check that previous line completes without exception true must beTrue // don't know how to just check that previous line completes without exception
} }
} }
"Analyzer plugin should send source begin and end" in { "Analyzer plugin should send source begin and end" in {

View File

@ -8,10 +8,16 @@ class TestLogger extends Logger
def debug(msg: F0[String]) = buffer("[debug] ", msg) def debug(msg: F0[String]) = buffer("[debug] ", msg)
def error(msg: F0[String]) = buffer("[error] ", msg) def error(msg: F0[String]) = buffer("[error] ", msg)
def verbose(msg: F0[String]) = buffer("[verbose] ", msg) def verbose(msg: F0[String]) = buffer("[verbose] ", msg)
def info(msg: => String) = buffer("[info] ", msg)
def warn(msg: => String) = buffer("[warn] ", msg)
def debug(msg: => String) = buffer("[debug] ", msg)
def error(msg: => String) = buffer("[error] ", msg)
def verbose(msg: => String) = buffer("[verbose] ", msg)
def show() { buffer.foreach(_()) } def show() { buffer.foreach(_()) }
def clear() { buffer.clear() } def clear() { buffer.clear() }
def trace(t: F0[Throwable]) { buffer += f0(t().printStackTrace) } def trace(t: F0[Throwable]) { buffer += f0(t().printStackTrace) }
private def buffer(s: String, msg: F0[String]) { buffer += f0(println(s + msg())) } private def buffer(s: String, msg: F0[String]) { buffer(s, msg()) }
private def buffer(s: String, msg: => String) { buffer += f0(println(s + msg)) }
} }
object TestLogger object TestLogger
{ {
@ -22,4 +28,11 @@ object TestLogger
catch { case e: Exception => log.show(); throw e } catch { case e: Exception => log.show(); throw e }
finally { log.clear() } finally { log.clear() }
} }
def apply[L <: TestLogger, T](newLogger: => L)(f: L => T): T =
{
val log = newLogger
try { f(log) }
catch { case e: Exception => log.show(); throw e }
finally { log.clear() }
}
} }

View File

@ -9,12 +9,16 @@ import xsbti.Versions
* version of Scala. * version of Scala.
* *
* The component manager provides services to install and retrieve components to the local repository. * The component manager provides services to install and retrieve components to the local repository.
* This is used for source jars so that the compilation need not be repeated for other projects on the same * This is used for compiled source jars so that the compilation need not be repeated for other projects on the same
* machine. * machine.
*/ */
class ComponentManager(baseDirectory: File, log: IvyLogger) extends NotNull class ComponentManager(baseDirectory: File, log: IvyLogger) extends NotNull
{ {
/** Get the location where files for component 'id' are stored. This method does not ensure that the component is retrieved from the
* local repository. By default, the location returned is is baseDirectory / id.*/
def location(id: String): File = new File(baseDirectory, id) def location(id: String): File = new File(baseDirectory, id)
/** Get the location where files for component 'id' are stored. If the component has not yet been retrieved from the local repository,
* it is retrieved first. */
def directory(id: String): File = def directory(id: String): File =
{ {
val dir = location(id) val dir = location(id)
@ -22,24 +26,38 @@ class ComponentManager(baseDirectory: File, log: IvyLogger) extends NotNull
update(id) update(id)
dir dir
} }
// get the contents of the given directory, wrapping a null result in an empty list.
private def contents(dir: File): Seq[File] = private def contents(dir: File): Seq[File] =
{ {
val fs = dir.listFiles val fs = dir.listFiles
if(fs == null) Nil else fs if(fs == null) Nil else fs
} }
/** Get all of the files for component 'id', throwing an exception if no files exist for the component. */
def files(id: String): Iterable[File] = def files(id: String): Iterable[File] =
{ {
val fs = contents(directory(id)) val fs = contents(directory(id))
if(!fs.isEmpty) fs else error("Could not find required component '" + id + "'") if(!fs.isEmpty) fs else invalid("Could not find required component '" + id + "'")
} }
/** Get the file for component 'id', throwing an exception if no files or multiple files exist for the component. */
def file(id: String): File = def file(id: String): File =
files(id).toList match { files(id).toList match {
case x :: Nil => x case x :: Nil => x
case xs => error("Expected single file for component '" + id + "', found: " + xs.mkString(", ")) case xs => invalid("Expected single file for component '" + id + "', found: " + xs.mkString(", "))
} }
private def invalid(msg: String) = throw new InvalidComponent(msg)
private def invalid(e: NotInCache) = throw new InvalidComponent(e.getMessage, e)
/** Retrieve the file for component 'id' from the local repository. */
def update(id: String): Unit = def update(id: String): Unit =
IvyActions.basicRetrieveLocal(sbtModuleID("manager"), Seq(sbtModuleID(id)), location(id), log) try { IvyCache.retrieveCachedJar(sbtModuleID(id), location(id), log) }
catch { case e: NotInCache => invalid(e) }
def sbtModuleID(id: String) = ModuleID("org.scala-tools.sbt", id, Versions.Sbt) def sbtModuleID(id: String) = ModuleID("org.scala-tools.sbt", id, Versions.Sbt)
def cache(id: String): Unit = IvyActions.basicPublishLocal(sbtModuleID(id), Nil, files(id), log) /** Install the files for component 'id' to the local repository. This is usually used after writing files to the directory returned by 'location'. */
def cache(id: String): Unit = IvyCache.cacheJar(sbtModuleID(id), file(id), log)
def clearCache(id: String): Unit = IvyCache.clearCachedJar(sbtModuleID(id), log)
}
class InvalidComponent(msg: String, cause: Throwable) extends RuntimeException(msg, cause)
{
def this(msg: String) = this(msg, null)
} }

View File

@ -207,7 +207,7 @@ private object IvySbt
moduleID.check() moduleID.check()
} }
/** Converts the given sbt module id into an Ivy ModuleRevisionId.*/ /** Converts the given sbt module id into an Ivy ModuleRevisionId.*/
private def toID(m: ModuleID) = private[xsbt] def toID(m: ModuleID) =
{ {
import m._ import m._
ModuleRevisionId.newInstance(organization, name, revision) ModuleRevisionId.newInstance(organization, name, revision)

View File

@ -6,6 +6,7 @@ import org.apache.ivy.{core, plugins, util, Ivy}
import core.cache.DefaultRepositoryCacheManager import core.cache.DefaultRepositoryCacheManager
import core.LogOptions import core.LogOptions
import core.deliver.DeliverOptions import core.deliver.DeliverOptions
import core.install.InstallOptions
import core.module.descriptor.{DefaultArtifact, DefaultDependencyArtifactDescriptor, MDArtifact} import core.module.descriptor.{DefaultArtifact, DefaultDependencyArtifactDescriptor, MDArtifact}
import core.module.descriptor.{DefaultDependencyDescriptor, DefaultModuleDescriptor, DependencyDescriptor, ModuleDescriptor} import core.module.descriptor.{DefaultDependencyDescriptor, DefaultModuleDescriptor, DependencyDescriptor, ModuleDescriptor}
import core.module.id.{ArtifactId,ModuleId, ModuleRevisionId} import core.module.id.{ArtifactId,ModuleId, ModuleRevisionId}
@ -18,28 +19,19 @@ final class UpdateConfiguration(val retrieveDirectory: File, val outputPattern:
object IvyActions object IvyActions
{ {
def basicPublishLocal(moduleID: ModuleID, dependencies: Iterable[ModuleID], artifactFiles: Iterable[File], log: IvyLogger) /** Installs the dependencies of the given 'module' from the resolver named 'from' to the resolver named 'to'.*/
def install(module: IvySbt#Module, from: String, to: String)
{ {
val artifacts = artifactFiles.map(Artifact.defaultArtifact) module.withModule { (ivy, md, default) =>
val (ivy, local) = basicLocalIvy(log) for(dependency <- md.getDependencies)
val module = new ivy.Module(ModuleConfiguration(moduleID, dependencies, artifacts)) {
val srcArtifactPatterns = artifactFiles.map(_.getAbsolutePath) module.logger.info("Installing " + dependency)
publish(module, local.name, srcArtifactPatterns, None, None) val options = new InstallOptions
options.setValidate(module.moduleConfiguration.validate)
options.setTransitive(dependency.isTransitive)
ivy.install(dependency.getDependencyRevisionId, from, to, options)
} }
def basicRetrieveLocal(moduleID: ModuleID, dependencies: Iterable[ModuleID], to: File, log: IvyLogger)
{
val (ivy, local) = basicLocalIvy(log)
val module = new ivy.Module(ModuleConfiguration(moduleID, dependencies, Nil))
val up = new UpdateConfiguration(to, defaultOutputPattern, false, true)
update(module, up)
} }
def defaultOutputPattern = "[artifact]-[revision](-[classifier]).[ext]"
private def basicLocalIvy(log: IvyLogger) =
{
val local = Resolver.defaultLocal
val paths = new IvyPaths(new File("."), None)
val conf = new IvyConfiguration(paths, Seq(local), log)
(new IvySbt(conf), local)
} }
/** Clears the Ivy cache, as configured by 'config'. */ /** Clears the Ivy cache, as configured by 'config'. */

103
ivy/IvyCache.scala Normal file
View File

@ -0,0 +1,103 @@
package xsbt
import java.io.File
import java.net.URL
import org.apache.ivy.{core, plugins, util}
import core.cache.{ArtifactOrigin, CacheDownloadOptions, DefaultRepositoryCacheManager}
import core.module.descriptor.{Artifact => IvyArtifact, DefaultArtifact}
import plugins.repository.file.{FileRepository=>IvyFileRepository, FileResource}
import plugins.repository.{ArtifactResourceResolver, Resource, ResourceDownloader}
import plugins.resolver.util.ResolvedResource
import util.FileUtil
class NotInCache(val id: ModuleID, cause: Throwable)
extends RuntimeException(NotInCache(id, cause), cause)
{
def this(id: ModuleID) = this(id, null)
}
private object NotInCache
{
def apply(id: ModuleID, cause: Throwable) =
{
val postfix = if(cause == null) "" else (": " +cause.toString)
"File for " + id + " not in cache" + postfix
}
}
/** Provides methods for working at the level of a single jar file with the default Ivy cache.*/
object IvyCache
{
/** Caches the given 'file' with the given ID. It may be retrieved or cleared using this ID.*/
def cacheJar(moduleID: ModuleID, file: File, log: IvyLogger)
{
val artifact = defaultArtifact(moduleID)
val resolved = new ResolvedResource(new FileResource(new IvyFileRepository, file), moduleID.revision)
withDefaultCache(log) { cache =>
val resolver = new ArtifactResourceResolver { def resolve(artifact: IvyArtifact) = resolved }
cache.download(artifact, resolver, new FileDownloader, new CacheDownloadOptions)
}
}
/** Clears the cache of the jar for the given ID.*/
def clearCachedJar(id: ModuleID, log: IvyLogger)
{
try { getCachedFile(id, log).delete }
catch { case e: Exception => log.debug("Error cleaning cached jar: " + e.toString) }
}
/** Copies the cached jar for the given ID to the directory 'toDirectory'. If the jar is not in the cache, NotInCache is thrown.*/
def retrieveCachedJar(id: ModuleID, toDirectory: File, log: IvyLogger) =
{
val cachedFile = getCachedFile(id, log)
val copyTo = new File(toDirectory, cachedFile.getName)
FileUtil.copy(cachedFile, copyTo, null)
copyTo
}
/** Get the location of the cached jar for the given ID in the Ivy cache. If the jar is not in the cache, NotInCache is thrown.*/
def getCachedFile(id: ModuleID, log: IvyLogger): File =
{
val cachedFile =
try
{
withDefaultCache(log) { cache =>
val artifact = defaultArtifact(id)
cache.getArchiveFileInCache(artifact, unknownOrigin(artifact))
}
}
catch { case e: Exception => throw new NotInCache(id, e) }
if(cachedFile.exists) cachedFile else throw new NotInCache(id)
}
/** Calls the given function with the default Ivy cache.*/
def withDefaultCache[T](log: IvyLogger)(f: DefaultRepositoryCacheManager => T): T =
{
val (ivy, local) = basicLocalIvy(log)
ivy.withIvy { ivy =>
val cache = ivy.getSettings.getDefaultRepositoryCacheManager.asInstanceOf[DefaultRepositoryCacheManager]
cache.setUseOrigin(false)
f(cache)
}
}
private def unknownOrigin(artifact: IvyArtifact) = ArtifactOrigin.unkwnown(artifact)
/** A minimal Ivy setup with only a local resolver and the current directory as the base directory.*/
private def basicLocalIvy(log: IvyLogger) =
{
val local = Resolver.defaultLocal
val paths = new IvyPaths(new File("."), None)
val conf = new IvyConfiguration(paths, Seq(local), log)
(new IvySbt(conf), local)
}
/** Creates a default jar artifact based on the given ID.*/
private def defaultArtifact(moduleID: ModuleID): IvyArtifact =
new DefaultArtifact(IvySbt.toID(moduleID), null, moduleID.name, "jar", "jar")
}
/** Required by Ivy for copying to the cache.*/
private class FileDownloader extends ResourceDownloader with NotNull
{
def download(artifact: IvyArtifact, resource: Resource, dest: File)
{
if(dest.exists()) dest.delete()
val part = new File(dest.getAbsolutePath + ".part")
FileUtil.copy(resource.openStream, part, null)
if(!part.renameTo(dest))
error("Could not move temporary file " + part + " to final location " + dest)
}
}

View File

@ -311,8 +311,8 @@ object Artifact
def apply(name: String, url: URL): Artifact =Artifact(name, extract(url, defaultType), extract(url, defaultExtension), None, Nil, Some(url)) def apply(name: String, url: URL): Artifact =Artifact(name, extract(url, defaultType), extract(url, defaultExtension), None, Nil, Some(url))
val defaultExtension = "jar" val defaultExtension = "jar"
val defaultType = "jar" val defaultType = "jar"
private[this] def extract(url: URL, default: String): String = extract(url.toString, default) def extract(url: URL, default: String): String = extract(url.toString, default)
private[this] def extract(name: String, default: String): String = def extract(name: String, default: String): String =
{ {
val i = name.lastIndexOf('.') val i = name.lastIndexOf('.')
if(i >= 0) if(i >= 0)

View File

@ -0,0 +1,95 @@
package xsbt
import java.io.File
import org.specs._
import FileUtilities.{createDirectory, delete, touch, withTemporaryDirectory}
import org.apache.ivy.util.ChecksumHelper
object ComponentManagerTest extends Specification
{
val TestID = "manager-test"
"Component manager" should {
"throw an exception if 'file' is called for a non-existing component" in {
withManager { _.file(TestID) must throwA[InvalidComponent] }
}
"throw an exception if 'file' is called for an empty component" in {
withManager { manager =>
createDirectory(manager.location(TestID))
( manager.file(TestID) ) must throwA[InvalidComponent]
}
}
"return the file for a single-file component" in {
withManager { manager =>
createFiles(manager, TestID, "a") match { case Seq(x) =>
manager.file(TestID).getAbsoluteFile must beEqualTo(x.getAbsoluteFile)
}
}
}
"throw an exception if 'file' is called for multi-file component" in {
withManager { manager =>
createFiles(manager, TestID, "a", "b")
( manager.file(TestID) ) must throwA[InvalidComponent]
}
}
"return the files for a multi-file component" in {
withManager { manager =>
val files = createFiles(manager, TestID, "a", "b")
manager.files(TestID) must haveTheSameElementsAs(files)
}
}
"return the files for a single-file component" in {
withManager { manager =>
val files = createFiles(manager, TestID, "a")
manager.files(TestID) must haveTheSameElementsAs(files)
}
}
"throw an exception if 'files' is called for a non-existing component" in {
withManager { _.files(TestID) must throwA[InvalidComponent] }
}
"properly cache a file and then retrieve it to an unresolved component" in {
withManager { manager =>
val file = createFile(manager, TestID, "a")
val hash = checksum(file)
try
{
manager.cache(TestID)
delete(manager.location(TestID))
FileUtilities.listFiles(manager.location(TestID)).toList must haveSize(0)
checksum(manager.file(TestID)) must beEqualTo(hash)
}
finally { manager.clearCache(TestID) }
}
}
"not retrieve to a component already resolved" in {
withManager { manager =>
val file = createFile(manager, TestID, "a")
try
{
manager.cache(TestID)
val idDirectory = manager.location(TestID)
delete(idDirectory)
createDirectory(idDirectory)
manager.file(TestID) must throwA[InvalidComponent]
}
finally { manager.clearCache(TestID) }
}
}
}
private def checksum(file: File) = ChecksumHelper.computeAsString(file, "sha1")
private def createFile(manager: ComponentManager, id: String, name: String): File = createFiles(manager, id, name).toList.head
private def createFiles(manager: ComponentManager, id: String, names: String*): Seq[File] =
{
val dir = manager.location(id)
createDirectory(dir)
names.map { name =>
val testFile = new File(dir, name)
touch(testFile)
testFile
}
}
private def withManager[T](f: ComponentManager => T): T =
TestIvyLogger( logger => withTemporaryDirectory { temp => f(new ComponentManager(temp, logger)) } )
}

View File

@ -0,0 +1,9 @@
package xsbt
import xsbti.TestLogger
class TestIvyLogger extends TestLogger with IvyLogger
object TestIvyLogger
{
def apply[T](f: TestIvyLogger => T): T = TestLogger(new TestIvyLogger)(f)
}

View File

@ -42,6 +42,9 @@ class XSbt(info: ProjectInfo) extends ParentProject(info)
class IvyProject(info: ProjectInfo) extends Base(info) class IvyProject(info: ProjectInfo) extends Base(info)
{ {
val ivy = "org.apache.ivy" % "ivy" % "2.0.0" val ivy = "org.apache.ivy" % "ivy" % "2.0.0"
// use IO from tests
override def testCompileAction = super.testCompileAction dependsOn(ioSub.testCompile)
override def testClasspath = super.testClasspath +++ ioSub.testClasspath
} }
class InterfaceProject(info: ProjectInfo) extends DefaultProject(info) class InterfaceProject(info: ProjectInfo) extends DefaultProject(info)
{ {

View File

@ -50,16 +50,27 @@ object FileUtilities
(name, "") (name, "")
} }
def createDirectory(dir: File): Unit = /** Creates a file at the given location.*/
translate("Could not create directory " + dir + ": ") def touch(file: File)
{ {
if(dir.exists) createDirectory(file.getParentFile)
{ val created = translate("Could not create file " + file) { file.createNewFile() }
if(!dir.isDirectory) if(created)
error("file exists and is not a directory.") ()
else if(file.isDirectory)
error("File exists and is a directory.")
else if(!file.setLastModified(System.currentTimeMillis))
error("Could not update last modified time for file " + file)
} }
else if(!dir.mkdirs()) def createDirectory(dir: File): Unit =
error("<unknown error>") {
def failBase = "Could not create directory " + dir
if(dir.isDirectory || dir.mkdirs())
()
else if(dir.exists)
error(failBase + ": file exists and is not a directory.")
else
error(failBase)
} }
def unzip(from: File, toDirectory: File): Set[File] = unzip(from, toDirectory, AllPassFilter) def unzip(from: File, toDirectory: File): Set[File] = unzip(from, toDirectory, AllPassFilter)
def unzip(from: File, toDirectory: File, filter: NameFilter): Set[File] = fileInputStream(from)(in => unzip(in, toDirectory, filter)) def unzip(from: File, toDirectory: File, filter: NameFilter): Set[File] = fileInputStream(from)(in => unzip(in, toDirectory, filter))
@ -157,7 +168,7 @@ object FileUtilities
create(0) create(0)
} }
private[xsbt] def jars(dir: File): Iterable[File] = wrapNull(dir.listFiles(GlobFilter("*.jar"))) private[xsbt] def jars(dir: File): Iterable[File] = listFiles(dir, GlobFilter("*.jar"))
def delete(files: Iterable[File]): Unit = files.foreach(delete) def delete(files: Iterable[File]): Unit = files.foreach(delete)
def delete(file: File) def delete(file: File)
@ -166,18 +177,22 @@ object FileUtilities
{ {
if(file.isDirectory) if(file.isDirectory)
{ {
delete(wrapNull(file.listFiles)) delete(listFiles(file))
file.delete file.delete
} }
else if(file.exists) else if(file.exists)
file.delete file.delete
} }
} }
private def wrapNull(a: Array[File]): Array[File] = def listFiles(dir: File, filter: FileFilter): Array[File] = wrapNull(dir.listFiles(filter))
def listFiles(dir: File): Array[File] = wrapNull(dir.listFiles())
private def wrapNull(a: Array[File]) =
{
if(a == null) if(a == null)
new Array[File](0) new Array[File](0)
else else
a a
}
/** Creates a jar file. /** Creates a jar file.
@ -219,7 +234,7 @@ object FileUtilities
if(sourceFile.isDirectory) if(sourceFile.isDirectory)
{ {
if(recursive) if(recursive)
wrapNull(sourceFile.listFiles).foreach(add) listFiles(sourceFile).foreach(add)
} }
else if(sourceFile.exists) else if(sourceFile.exists)
{ {