From a70ddd8e32f48277af4a1cd1a373c466234087a0 Mon Sep 17 00:00:00 2001 From: Mark Harrah Date: Tue, 18 Aug 2009 23:25:34 -0400 Subject: [PATCH] Tests and fixes for analysis plugin and the task scheduler. --- compile/interface/Analyzer.scala | 63 ++++++---- .../src/test/scala/ApplicationsTest.scala | 117 ++++++++++++++++++ .../interface/src/test/scala/CheckBasic.scala | 56 ++------- .../src/test/scala/DetectSubclasses.scala | 33 +++++ .../src/test/scala/TestCompile.scala | 7 ++ .../src/main/java/xsbti/AnalysisCallback.java | 4 +- project/build/XSbt.scala | 6 +- tasks/TaskScheduler.scala | 19 +-- tasks/src/test/scala/TaskRunnerCircular.scala | 26 +--- tasks/src/test/scala/TaskRunnerFork.scala | 6 + 10 files changed, 227 insertions(+), 110 deletions(-) create mode 100644 compile/interface/src/test/scala/ApplicationsTest.scala create mode 100644 compile/interface/src/test/scala/DetectSubclasses.scala diff --git a/compile/interface/Analyzer.scala b/compile/interface/Analyzer.scala index bd45d7721..a1a7a5f58 100644 --- a/compile/interface/Analyzer.scala +++ b/compile/interface/Analyzer.scala @@ -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 } \ No newline at end of file diff --git a/compile/interface/src/test/scala/ApplicationsTest.scala b/compile/interface/src/test/scala/ApplicationsTest.scala new file mode 100644 index 000000000..8be9fc955 --- /dev/null +++ b/compile/interface/src/test/scala/ApplicationsTest.scala @@ -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]()) + } +} \ No newline at end of file diff --git a/compile/interface/src/test/scala/CheckBasic.scala b/compile/interface/src/test/scala/CheckBasic.scala index 7af655d13..e3a41b381 100644 --- a/compile/interface/src/test/scala/CheckBasic.scala +++ b/compile/interface/src/test/scala/CheckBasic.scala @@ -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) } } } diff --git a/compile/interface/src/test/scala/DetectSubclasses.scala b/compile/interface/src/test/scala/DetectSubclasses.scala new file mode 100644 index 000000000..a502bbe50 --- /dev/null +++ b/compile/interface/src/test/scala/DetectSubclasses.scala @@ -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")) + } + } + } +} \ No newline at end of file diff --git a/compile/interface/src/test/scala/TestCompile.scala b/compile/interface/src/test/scala/TestCompile.scala index 29135832b..013dd4188 100644 --- a/compile/interface/src/test/scala/TestCompile.scala +++ b/compile/interface/src/test/scala/TestCompile.scala @@ -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 => diff --git a/interface/src/main/java/xsbti/AnalysisCallback.java b/interface/src/main/java/xsbti/AnalysisCallback.java index 2ceedd4a6..70870965c 100644 --- a/interface/src/main/java/xsbti/AnalysisCallback.java +++ b/interface/src/main/java/xsbti/AnalysisCallback.java @@ -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 source depends on the source file - * dependsOn.*/ + * dependsOn. 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 source depends on the jar * jar.*/ diff --git a/project/build/XSbt.scala b/project/build/XSbt.scala index c6daa57de..96a11a673 100644 --- a/project/build/XSbt.scala +++ b/project/build/XSbt.scala @@ -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" diff --git a/tasks/TaskScheduler.scala b/tasks/TaskScheduler.scala index a04406cf8..9c2f47a9f 100644 --- a/tasks/TaskScheduler.scala +++ b/tasks/TaskScheduler.scala @@ -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) diff --git a/tasks/src/test/scala/TaskRunnerCircular.scala b/tasks/src/test/scala/TaskRunnerCircular.scala index 8034b47e4..795875fb2 100644 --- a/tasks/src/test/scala/TaskRunnerCircular.scala +++ b/tasks/src/test/scala/TaskRunnerCircular.scala @@ -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 } - } } \ No newline at end of file diff --git a/tasks/src/test/scala/TaskRunnerFork.scala b/tasks/src/test/scala/TaskRunnerFork.scala index b1b49e00f..cffaffa1b 100644 --- a/tasks/src/test/scala/TaskRunnerFork.scala +++ b/tasks/src/test/scala/TaskRunnerFork.scala @@ -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 } )