Tests and fixes for analysis plugin and the task scheduler.

This commit is contained in:
Mark Harrah 2009-08-18 23:25:34 -04:00
parent ec7074a340
commit a70ddd8e32
10 changed files with 227 additions and 110 deletions

View File

@ -15,13 +15,13 @@ import xsbti.{AnalysisCallback, AnalysisCallbackContainer}
class Analyzer(val global: Global) extends Plugin
{
val callback = global.asInstanceOf[AnalysisCallbackContainer].analysisCallback
import global._
val name = "xsbt-analyze"
val description = "A plugin to find all concrete instances of a given class and extract dependency information."
val components = List[PluginComponent](Component)
/* ================================================== */
// These two templates abuse scope for source compatibility between Scala 2.7.x and 2.8.x so that a single
// sbt codebase compiles with both series of versions.
@ -39,7 +39,7 @@ class Analyzer(val global: Global) extends Plugin
override val runsAfter = afterPhase :: runsBefore
}
/* ================================================== */
private object Component extends CompatiblePluginComponent("jvm")
{
val global = Analyzer.this.global
@ -53,16 +53,8 @@ class Analyzer(val global: Global) extends Plugin
def run
{
val outputDirectory = new File(global.settings.outdir.value)
val superclassNames = callback.superclassNames.map(newTermName)
val superclassesAll =
for(name <- superclassNames) yield
{
try { Some(global.definitions.getClass(name)) }
catch { case fe: scala.tools.nsc.FatalError => callback.superclassNotFound(name.toString); None }
}
val superclasses = superclassesAll.filter(_.isDefined).map(_.get)
//println("Superclass names: " + superclassNames.mkString(", ") + "\n\tall: " + superclasses.mkString(", "))
val superclasses = callback.superclassNames flatMap(classForName)
for(unit <- currentRun.units)
{
// build dependencies structure
@ -90,7 +82,7 @@ class Analyzer(val global: Global) extends Plugin
else
callback.sourceDependency(onSource.file, sourceFile)
}
// find subclasses and modules with main methods
for(clazz @ ClassDef(mods, n, _, _) <- unit.body)
{
@ -105,7 +97,7 @@ class Analyzer(val global: Global) extends Plugin
callback.foundApplication(sourceFile, sym.fullNameString)
}
}
// build list of generated classes
for(iclass <- unit.icode)
{
@ -129,11 +121,25 @@ class Analyzer(val global: Global) extends Plugin
}
}
}
private def classForName(name: String) =
{
try
{
if(name.indexOf('.') < 0)
{
val sym = definitions.EmptyPackageClass.info.member(newTypeName(name))
if(sym != NoSymbol) Some( sym ) else { callback.superclassNotFound(name); None }
}
else
Some( global.definitions.getClass(newTermName(name)) )
}
catch { case fe: scala.tools.nsc.FatalError => callback.superclassNotFound(name); None }
}
private def classFile(sym: Symbol): Option[AbstractFile] =
{
import scala.tools.nsc.symtab.Flags
val name = sym.fullNameString(java.io.File.separatorChar) + (if (sym.hasFlag(Flags.MODULE)) "$" else "")
val name = sym.fullNameString(File.separatorChar) + (if (sym.hasFlag(Flags.MODULE)) "$" else "")
val entry = classPath.root.find(name, false)
if (entry ne null)
Some(entry.classFile)
@ -148,7 +154,7 @@ class Analyzer(val global: Global) extends Plugin
else
None
}
private def isTopLevelModule(sym: Symbol): Boolean =
atPhase (currentRun.picklerPhase.next) {
sym.isModuleClass && !sym.isImplClass && !sym.isNestedClass
@ -169,29 +175,32 @@ class Analyzer(val global: Global) extends Plugin
else
new File(packageFile(outputDirectory, s.owner.enclClass), s.simpleName.toString)
}
private def hasMainMethod(sym: Symbol): Boolean =
{
val main = sym.info.nonPrivateMember(newTermName("main"))//nme.main)
main.tpe match
{
case OverloadedType(pre, alternatives) => alternatives.exists(alt => isVisible(alt) && isMainType(pre.memberType(alt)))
case tpe => isVisible(main) && isMainType(main.owner.thisType.memberType(main))
atPhase(currentRun.typerPhase.next) {
main.tpe match
{
case OverloadedType(pre, alternatives) => alternatives.exists(alt => isVisible(alt) && isMainType(pre.memberType(alt)))
case tpe => isVisible(main) && isMainType(main.owner.thisType.memberType(main))
}
}
}
private def isVisible(sym: Symbol) = sym != NoSymbol && sym.isPublic && !sym.isDeferred
private def isMainType(tpe: Type) =
private def isMainType(tpe: Type): Boolean =
{
tpe match
{
// singleArgument is of type Symbol in 2.8.0 and type Type in 2.7.x
case MethodType(List(singleArgument), result) => isUnitType(result) && isStringArray(singleArgument)
case _ => false
case PolyType(typeParams, result) => isMainType(result)
case _ => false
}
}
private lazy val StringArrayType = appliedType(definitions.ArrayClass.typeConstructor, definitions.StringClass.tpe :: Nil)
// isStringArray is overloaded to handle the incompatibility between 2.7.x and 2.8.0
private def isStringArray(tpe: Type): Boolean = tpe.typeSymbol == StringArrayType.typeSymbol
private def isStringArray(tpe: Type): Boolean = tpe =:= StringArrayType
private def isStringArray(sym: Symbol): Boolean = isStringArray(sym.tpe)
private def isUnitType(tpe: Type) = tpe.typeSymbol == definitions.UnitClass
}

View File

@ -0,0 +1,117 @@
package xsbt
import java.io.File
import java.net.URLClassLoader
import org.specs.Specification
/** Verifies that the analyzer plugin properly detects main methods. The main method must be
* public with the right signature and be defined on a public, top-level module.*/
object ApplicationsTest extends Specification
{
val sourceContent =
"""
object Main { def main(args: Array[String]) {} }
""" :: """
class Main2 { def main(args: Array[String]) {} }
""" :: """
object Main3 { private def main(args: Array[String]) {} }
private object Main3b extends Main2
object Main3c { private def main(args: Array[String]) {} }
protected object Main3d { def main(args: Array[String]) {} }
object Main3e {
protected def main(args: Array[String]) {}
}
package a {
object Main3f { private[a] def main(args: Array[String]) {} }
object Main3g { protected[a] def main(args: Array[String]) {} }
}
""" ::"""
object Main4 extends Main2
""" :: """
trait Main5 { def main(args: Array[String]) {} }; trait Main5b extends Main5; trait Main5c extends Main2; abstract class Main5d { def main(args: Array[String]) {} }
""" :: """
object Main6a { var main = () }
object Main6b { var main = (args: Array[String]) => () }
""" :: """
object Main7 { object Main7b extends Main2 }
""" :: """
object Main8 extends Main2 { object Main7b extends Main2 }
""" :: """
object Main9 {
def main() {}
def main(i: Int) {}
def main(args: Array[String]) {}
}
""" :: """
object MainA {
def main() {}
def main(i: Int) {}
def main(args: Array[String], other: String) {}
def main(i: Array[Int]) {}
}
object MainA2 {
def main[T](args: Array[T]) {}
}
""" :: """
object MainB extends Main2 {
def main() {}
def main(i: Int) {}
}
""" :: """
object MainC1 {
def main(args: Array[String]) = 3
}
object MainC2 {
def main1(args: Array[String]) {}
}
""" :: """
object MainD1 {
val main = ()
}
object MainD2 {
val main = (args: Array[String]) => ()
}
""" :: """
object MainE1 {
type T = String
def main(args: Array[T]) {}
}
object MainE2 {
type AT = Array[String]
def main(args: AT) {}
}
object MainE3 {
type U = Unit
type T = String
def main(args: Array[T]): U = ()
}
object MainE4 {
def main[T](args: Array[String]) {}
}
object MainE5 {
type A[T] = Array[String]
def main[T](args: A[T]) {}
}
""" ::
Nil
val sources = for((source, index) <- sourceContent.zipWithIndex) yield new File("Main" + (index+1) + ".scala") -> source
"Analysis plugin should detect applications" in {
WithFiles(sources : _*) { case files @ Seq(main, main2, main3, main4, main5, main6, main7, main8, main9, mainA, mainB, mainC, mainD, mainE) =>
CallbackTest(files, Nil) { (callback, file, log) =>
val expected = Seq( main -> "Main", main4 -> "Main4", main8 -> "Main8", main9 -> "Main9", mainB -> "MainB",
mainE -> "MainE1", mainE -> "MainE2", mainE -> "MainE3", mainE -> "MainE4", mainE -> "MainE5" )
(callback.applications) must haveTheSameElementsAs(expected)
val loader = new URLClassLoader(Array(file.toURI.toURL), getClass.getClassLoader)
for( (_, className) <- expected) testRun(loader, className)
}
}
}
private def testRun(loader: ClassLoader, className: String)
{
val obj = Class.forName(className+"$", true, loader)
val singletonField = obj.getField("MODULE$")
val singleton = singletonField.get(null)
singleton.asInstanceOf[{def main(args: Array[String]): Unit}].main(Array[String]())
}
}

View File

@ -7,60 +7,18 @@ object CheckBasic extends Specification
{
val basicName = new File("Basic.scala")
val basicSource = "package org.example { object Basic }"
val mainName = new File("Main.scala")
val mainSource = "object Main { def main(args: Array[String]) {} }"
val super1Name = new File("a/Super.scala")
val super2Name = new File("a/Super2.scala")
val midName = new File("b/Middle.scala")
val sub1Name = new File("b/SubA.scala")
val sub2Name = new File("b/SubB.scala")
val sub3Name = new File("SubC.scala")
val super1Source = "package a; trait Super"
val super2Source = "class Super2"
val midSource = "package y.w; trait Mid extends a.Super"
val subSource1 = "package a; trait Sub1 extends y.w.Mid"
val subSource2 = "trait Sub2 extends a.Super"
val subSource3 = "private class F extends a.Super; package c { object Sub3 extends Super2 }"
"Compiling basic file should succeed" in {
WithFiles(basicName -> basicSource){ files =>
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
}
}
"Analysis plugin" should {
"send source begin and end" in {
WithFiles(basicName -> basicSource) { files =>
CallbackTest(files) { callback =>
(callback.beganSources) must haveTheSameElementsAs(files)
(callback.endedSources) must haveTheSameElementsAs(files)
}
}
}
"detect applications" in {
WithFiles(mainName -> mainSource ) { files =>
CallbackTest(files) { callback =>
(callback.applications) must haveTheSameElementsAs(files.map(file => (file, "Main")))
}
}
}
"detect subclasses" in {
WithFiles(super1Name -> super1Source, midName -> midSource, sub1Name -> subSource1, sub2Name -> subSource2,
super2Name -> super2Source, sub3Name -> subSource3)
{
case files @ Seq(supFile, midFile, sub1File, sub2File, sup2File, sub3File) =>
CallbackTest(files,Seq( "a.Super", "Super2", "x.Super3")) { (callback, ignore, ignore2) =>
val expected = (sub1File, "a.Super", "a.Sub1", false) :: (sub2File, "a.Super", "a.Sub2", false) ::
(sub3File, "Super2", "Sub3", true) :: Nil
//println(callback.foundSubclasses)
//println(callback.invalidSuperclasses)
(callback.foundSubclasses) must haveTheSameElementsAs(expected)
(callback.invalidSuperclasses) must haveTheSameElementsAs(Seq("x.Super3"))
}
"Analyzer plugin should send source begin and end" in {
WithFiles(basicName -> basicSource) { files =>
CallbackTest(files) { callback =>
(callback.beganSources) must haveTheSameElementsAs(files)
(callback.endedSources) must haveTheSameElementsAs(files)
}
}
}

View File

@ -0,0 +1,33 @@
package xsbt
import java.io.File
import org.specs.Specification
object DetectSubclasses extends Specification
{
val sources =
("a/Super.scala" -> "package a; trait Super") ::
("a/Super2.scala" -> "class Super2") ::
("b/Middle.scala" -> "package y.w; trait Mid extends a.Super") ::
("b/Sub1.scala" -> "package a; class Sub1 extends y.w.Mid") ::
("b/Sub2.scala" -> "final class Sub2 extends a.Super") ::
("Sub3.scala" -> "private class F extends a.Super; package c { object Sub3 extends Super2 }") ::
Nil
"Analysis plugin should detect subclasses" in {
WithFiles(sources.map{case (file, content) => (new File(file), content)} : _*)
{
case files @ Seq(supFile, sup2File, midFile, sub1File, sub2File, sub3File) =>
CallbackTest(files, Seq( "a.Super", "Super2", "x.Super3", "Super4") ) { (callback, x, xx) =>
val expected =
(sub1File, "a.Sub1", "a.Super", false) ::
(sub2File, "Sub2", "a.Super", false) ::
(sup2File, "Super2", "Super2", false) ::
(sub3File, "c.Sub3", "Super2", true) ::
Nil
(callback.foundSubclasses) must haveTheSameElementsAs(expected)
(callback.invalidSuperclasses) must haveTheSameElementsAs(Seq("x.Super3", "Super4"))
}
}
}
}

View File

@ -7,6 +7,8 @@ import FileUtilities.{classLocationFile, withTemporaryDirectory, write}
object TestCompile
{
/** Tests running the compiler interface with the analyzer plugin with a test callback. The test callback saves all information
* that the plugin sends it for post-compile analysis by the provided function.*/
def apply[T](arguments: Seq[String], superclassNames: Seq[String])(f: (TestCallback, Logger) => T): T =
{
val pluginLocation = classLocationFile[Analyzer]
@ -21,6 +23,8 @@ object TestCompile
f(testCallback, log)
}
}
/** Tests running the compiler interface with the analyzer plugin. The provided function is given a ClassLoader that can
* load the compiled classes..*/
def apply[T](sources: Seq[File])(f: ClassLoader => T): T =
CallbackTest.apply(sources, Nil){ case (callback, outputDir, log) => f(new URLClassLoader(Array(outputDir.toURI.toURL))) }
}
@ -38,6 +42,9 @@ object CallbackTest
}
object WithFiles
{
/** Takes the relative path -> content pairs and writes the content to a file in a temporary directory. The written file
* path is the relative path resolved against the temporary directory path. The provided function is called with the resolved file paths
* in the same order as the inputs. */
def apply[T](sources: (File, String)*)(f: Seq[File] => T): T =
{
withTemporaryDirectory { dir =>

View File

@ -17,7 +17,9 @@ public interface AnalysisCallback
* discovered.*/
public void foundSubclass(File source, String subclassName, String superclassName, boolean isModule);
/** Called to indicate that the source file <code>source</code> depends on the source file
* <code>dependsOn</code>.*/
* <code>dependsOn</code>. Note that only source files included in the current compilation will
* passed to this method. Dependencies on classes generated by sources not in the current compilation will
* be passed as class dependencies to the classDependency method.*/
public void sourceDependency(File dependsOn, File source);
/** Called to indicate that the source file <code>source</code> depends on the jar
* <code>jar</code>.*/

View File

@ -21,7 +21,7 @@ class XSbt(info: ProjectInfo) extends ParentProject(info)
def utilPath = path("util")
def compilePath = path("compile")
class CommonDependencies(info: ProjectInfo) extends DefaultProject(info)
{
val sc = "org.scala-tools.testing" % "scalacheck" % "1.5" % "test->default"
@ -35,7 +35,7 @@ class XSbt(info: ProjectInfo) extends ParentProject(info)
{
//override def compileOptions = super.compileOptions ++ List(Unchecked,ExplainTypes, CompileOption("-Xlog-implicits"))
}
class Base(info: ProjectInfo) extends DefaultProject(info) with AssemblyProject
class Base(info: ProjectInfo) extends DefaultProject(info)
{
override def scratch = true
}
@ -53,7 +53,7 @@ class XSbt(info: ProjectInfo) extends ParentProject(info)
// these set up the test so that the classes and resources are both in the output resource directory
// the main output path is removed so that the plugin (xsbt.Analyzer) is found in the output resource directory so that
// the tests can configure that directory as -Xpluginsdir (which requires the scalac-plugin.xml and the classes to be together)
override def testCompileAction = super.testCompileAction dependsOn(packageForTest)
override def testCompileAction = super.testCompileAction dependsOn(packageForTest, ioSub.testCompile)
override def mainResources = super.mainResources +++ "scalac-plugin.xml"
override def testClasspath = (super.testClasspath --- super.mainCompilePath) +++ ioSub.testClasspath +++ testPackagePath
def testPackagePath = outputPath / "test.jar"

View File

@ -58,12 +58,12 @@ private final class TaskScheduler[O](root: Task[O], strategy: ScheduleStrategy[W
private val strategyRun = strategy.run
private val failed = new mutable.HashSet[Task[_]]
private val failureReports = new mutable.ArrayBuffer[WorkFailure[Task[_]]]
{
val initialized = addGraph(root, root) // TODO: replace second root with something better? (it is ignored here anyway)
assert(initialized)
}
private def addReady[O](m: Task[O])
{
def add[I](m: ITask[I,O])
@ -72,7 +72,7 @@ private final class TaskScheduler[O](root: Task[O], strategy: ScheduleStrategy[W
strategyRun.workReady(new Work(m, input))
listener.runnable(m)
}
assert(!forwardDeps.contains(m), m)
assert(reverseDeps.contains(m), m)
assert(!completed.contains(m), m)
@ -172,20 +172,25 @@ private final class TaskScheduler[O](root: Task[O], strategy: ScheduleStrategy[W
retire(dependsOnM, None)
}
}
private def success[O](task: Task[O], value: Result[O]): Unit =
value match
{
case NewTask(t) =>
if(t == task)
if(t eq task)
{
failureReports += WorkFailure(t, CircularDependency(t, task))
retire(task, None)
}
else if(addGraph(t, task))
{
calls(t) = task
listener.calling(task, t)
if(completed.contains(t))
retire(task, Some(completed(t)))
else
{
calls(t) = task
listener.calling(task, t)
}
}
else
retire(task, None)

View File

@ -8,12 +8,9 @@ object TaskRunnerCircularTest extends Properties("TaskRunner Circular")
specify("Catches circular references", (intermediate: Int, workers: Int) =>
(workers > 0 && intermediate >= 0) ==> checkCircularReferences(intermediate, workers)
)
/*specify("Check root complete", (intermediate: Int, workers: Int) =>
(workers > 0 && intermediate >= 0) ==> checkRootComplete(intermediate, workers)
)*/
specify("Allows noncircular references", (intermediate: Int, workers: Int) =>
specify("Allows references to completed tasks", forAllNoShrink(Arbitrary.arbitrary[(Int,Int)])(x=>x) { case (intermediate: Int, workers: Int) =>
(workers > 0 && intermediate >= 0) ==> allowedReference(intermediate, workers)
)
})
final def allowedReference(intermediate: Int, workers: Int) =
{
val top = Task(intermediate) named("top")
@ -24,7 +21,7 @@ object TaskRunnerCircularTest extends Properties("TaskRunner Circular")
else
iterate(Task(t-1) named (t-1).toString)
}
try { checkResult(TaskRunner(iterate(top), workers), 0) }
try { checkResult(TaskRunner(iterate(top), workers), intermediate) }
catch { case e: CircularDependency => ("Unexpected exception: " + e) |: false }
}
final def checkCircularReferences(intermediate: Int, workers: Int) =
@ -44,21 +41,4 @@ object TaskRunnerCircularTest extends Properties("TaskRunner Circular")
try { TaskRunner(top, workers); false }
catch { case TasksFailed(failures) => failures.exists(_.exception.isInstanceOf[CircularDependency]) }
}
final def checkRootComplete(intermediate: Int, workers: Int) =
{
val top = Task(intermediate)
def iterate(task: Task[Int]): Task[Int] =
{
lazy val it: Task[Int] =
task bind { t =>
if(t <= 0)
it
else
iterate(Task(t-1) named (t-1).toString)
} named("it")
it
}
try { TaskRunner(iterate(top), workers); false }
catch { case e: CircularDependency => true }
}
}

View File

@ -14,6 +14,12 @@ object TaskRunnerForkTest extends Properties("TaskRunner Fork")
true
}
)
specify("Fork and reduce 2", (m: Int, workers: Int) =>
(workers > 0 && m > 1) ==> {
val task = (0 to m) fork {_ * 10} reduce{_ + _}
checkResult(TaskRunner(task, workers), 5*(m+1)*m)
}
)
specify("Double join", (a: Int, b: Int, workers: Int) =>
(workers > 0) ==> { runDoubleJoin(abs(a),abs(b),workers); true }
)