From d9f98c2848dbb0ae18ae75159761598fdac894c3 Mon Sep 17 00:00:00 2001 From: Guillaume Martres Date: Tue, 14 Mar 2017 18:52:23 +0100 Subject: sbt-bridge: Synchronize unit tests with sbt 0.13.14 Also fix a bug where the compiler output for the tests ended up in the wrong directory, causing some new tests from 0.13.14 to fail. --- sbt-bridge/test/xsbt/ExtractAPISpecification.scala | 80 ++++++++++++++++++++-- .../test/xsbt/ExtractUsedNamesSpecification.scala | 39 +++++++++++ .../test/xsbt/ScalaCompilerForUnitTesting.scala | 47 +++++++++---- sbt-bridge/test/xsbti/TestCallback.scala | 2 +- 4 files changed, 148 insertions(+), 20 deletions(-) (limited to 'sbt-bridge') diff --git a/sbt-bridge/test/xsbt/ExtractAPISpecification.scala b/sbt-bridge/test/xsbt/ExtractAPISpecification.scala index f5af67e45..4b3b2c51a 100644 --- a/sbt-bridge/test/xsbt/ExtractAPISpecification.scala +++ b/sbt-bridge/test/xsbt/ExtractAPISpecification.scala @@ -2,9 +2,8 @@ package xsbt import org.junit.runner.RunWith -import xsbti.api.ClassLike -import xsbti.api.Def -import xsbt.api.ShowAPI +import xsbti.api._ +import xsbt.api.DefaultShowAPI import org.specs2.mutable.Specification import org.specs2.runner.JUnitRunner @@ -17,7 +16,7 @@ class ExtractAPISpecification extends Specification { def stableExistentialNames: Boolean = { def compileAndGetFooMethodApi(src: String): Def = { - val compilerForTesting = new ScalaCompilerForUnitTesting + val compilerForTesting = new ScalaCompilerForUnitTesting(nameHashing = false) val sourceApi = compilerForTesting.extractApiFromSrc(src) val FooApi = sourceApi.definitions().find(_.name() == "Foo").get.asInstanceOf[ClassLike] val fooMethodApi = FooApi.structure().declared().find(_.name == "foo").get @@ -38,8 +37,81 @@ class ExtractAPISpecification extends Specification { | }""".stripMargin val fooMethodApi2 = compileAndGetFooMethodApi(src2) + fooMethodApi1 == fooMethodApi2 // Fails because xsbt.api is compiled with Scala 2.10 // SameAPI.apply(fooMethodApi1, fooMethodApi2) } + + /** + * Checks if representation of the inherited Namer class (with a declared self variable) in Global.Foo + * is stable between compiling from source and unpickling. We compare extracted APIs of Global when Global + * is compiled together with Namers or Namers is compiled first and then Global refers + * to Namers by unpickling types from class files. + * + * See https://github.com/sbt/sbt/issues/2504 + */ + "Self variable and no self type" in { + def selectNamer(api: SourceAPI): ClassLike = { + def selectClass(defs: Iterable[Definition], name: String): ClassLike = defs.collectFirst { + case cls: ClassLike if cls.name == name => cls + }.get + val global = selectClass(api.definitions, "Global") + val foo = selectClass(global.structure.declared, "Global.Foo") + selectClass(foo.structure.inherited, "Namers.Namer") + } + val src1 = + """|class Namers { + | class Namer { thisNamer => } + |} + |""".stripMargin + val src2 = + """|class Global { + | class Foo extends Namers + |} + |""".stripMargin + val compilerForTesting = new ScalaCompilerForUnitTesting(nameHashing = false) + val apis = compilerForTesting.extractApisFromSrcs(reuseCompilerInstance = false)(List(src1, src2), List(src2)) + val _ :: src2Api1 :: src2Api2 :: Nil = apis.toList + val namerApi1 = selectNamer(src2Api1) + val namerApi2 = selectNamer(src2Api2) + + DefaultShowAPI(namerApi1) == DefaultShowAPI(namerApi2) + // Fails because xsbt.api is compiled with Scala 2.10 + // SameAPI(namerApi1, namerApi2) + } + + /** + * Checks if self type is properly extracted in various cases of declaring a self type + * with our without a self variable. + */ + "Self type" in { + def collectFirstClass(defs: Array[Definition]): ClassLike = defs.collectFirst { + case c: ClassLike => c + }.get + val srcX = "trait X" + val srcY = "trait Y" + val srcC1 = "class C1 { this: C1 => }" + val srcC2 = "class C2 { thisC: C2 => }" + val srcC3 = "class C3 { this: X => }" + val srcC4 = "class C4 { thisC: X => }" + val srcC5 = "class C5 extends AnyRef with X with Y { self: X with Y => }" + val srcC6 = "class C6 extends AnyRef with X { self: X with Y => }" + // val srcC7 = "class C7 { _ => }" // DOTTY: Syntax not supported + val srcC8 = "class C8 { self => }" + val compilerForTesting = new ScalaCompilerForUnitTesting(nameHashing = false) + val apis = compilerForTesting.extractApisFromSrcs(reuseCompilerInstance = true)( + List(srcX, srcY, srcC1, srcC2, srcC3, srcC4, srcC5, srcC6, srcC8) + ).map(x => collectFirstClass(x.definitions)) + val emptyType = new EmptyType + def hasSelfType(c: ClassLike): Boolean = + c.selfType != emptyType + val (withSelfType, withoutSelfType) = apis.partition(hasSelfType) + // DOTTY: In the scalac ExtractAPI phase, the self-type is only + // extracted if it differs from the type of the class for stability + // reasons. This isn't necessary in dotty because we always pickle + // the self type. + withSelfType.map(_.name).toSet === Set("C1", "C2", "C3", "C4", "C5", "C6", "C8") + withoutSelfType.map(_.name).toSet === Set("X", "Y") + } } diff --git a/sbt-bridge/test/xsbt/ExtractUsedNamesSpecification.scala b/sbt-bridge/test/xsbt/ExtractUsedNamesSpecification.scala index ed463a3e6..6cff284fe 100644 --- a/sbt-bridge/test/xsbt/ExtractUsedNamesSpecification.scala +++ b/sbt-bridge/test/xsbt/ExtractUsedNamesSpecification.scala @@ -75,6 +75,7 @@ class ExtractUsedNamesSpecification extends Specification { |}""".stripMargin val compilerForTesting = new ScalaCompilerForUnitTesting(nameHashing = true) val usedNames = compilerForTesting.extractUsedNamesFromSrc(srcA, srcB) + // DOTTY TODO: "Int" is not actually used, but we collect it because // it's the inferred return type so it appears in a TypeTree // We could avoid this by checking if the untyped tree has a return type @@ -84,6 +85,44 @@ class ExtractUsedNamesSpecification extends Specification { usedNames === expectedNames } + "extract names in the types of trees" in { + val src1 = """|class X0 + |class X1 extends X0 + |class Y + |class A { + | type T >: X1 <: X0 + |} + |class M + |class N + |class P0 + |class P1 extends P0 + |object B { + | type S = Y + | val lista: List[A] = ??? + | val at: A#T = ??? + | val as: S = ??? + | def foo(m: M): N = ??? + | def bar[Param >: P1 <: P0](p: Param): Param = ??? + |}""".stripMargin + val src2 = """|object Test { + | val x = B.lista + | val y = B.at + | val z = B.as + | B.foo(???) + | B.bar(???) + |}""".stripMargin + val compilerForTesting = new ScalaCompilerForUnitTesting(nameHashing = true) + val usedNames = compilerForTesting.extractUsedNamesFromSrc(src1, src2) + val expectedNames = standardNames ++ Set("Test", "Test$", "B", "B$", + "Predef", "Predef$", "$qmark$qmark$qmark", "Nothing", + "lista", "List", "A", + "at", "T", "X1", "X0", + "as", "S", "Y", + "foo", "M", "N", + "bar", "P1", "P0") + usedNames === expectedNames + } + // test for https://github.com/gkossakowski/sbt/issues/3 "used names from the same compilation unit" in { val src = "class A { def foo: Int = 0; def bar: Int = foo }" diff --git a/sbt-bridge/test/xsbt/ScalaCompilerForUnitTesting.scala b/sbt-bridge/test/xsbt/ScalaCompilerForUnitTesting.scala index 409729023..a6b9fa65e 100644 --- a/sbt-bridge/test/xsbt/ScalaCompilerForUnitTesting.scala +++ b/sbt-bridge/test/xsbt/ScalaCompilerForUnitTesting.scala @@ -7,7 +7,7 @@ import _root_.scala.tools.nsc.reporters.ConsoleReporter import _root_.scala.tools.nsc.Settings import xsbti._ import xsbti.api.SourceAPI -import sbt.IO.withTemporaryDirectory +import sbt.IO._ import xsbti.api.ClassLike import xsbti.api.Definition import xsbti.api.Def @@ -21,7 +21,7 @@ import ScalaCompilerForUnitTesting.ExtractedSourceDependencies * Provides common functionality needed for unit tests that require compiling * source code using Scala compiler. */ -class ScalaCompilerForUnitTesting(nameHashing: Boolean = false) { +class ScalaCompilerForUnitTesting(nameHashing: Boolean, includeSynthToNameHashing: Boolean = false) { import scala.language.reflectiveCalls /** @@ -33,6 +33,15 @@ class ScalaCompilerForUnitTesting(nameHashing: Boolean = false) { analysisCallback.apis(tempSrcFile) } + /** + * Compiles given source code using Scala compiler and returns API representation + * extracted by ExtractAPI class. + */ + def extractApisFromSrcs(reuseCompilerInstance: Boolean)(srcs: List[String]*): Seq[SourceAPI] = { + val (tempSrcFiles, analysisCallback) = compileSrcs(srcs.toList, reuseCompilerInstance) + tempSrcFiles.map(analysisCallback.apis) + } + def extractUsedNamesFromSrc(src: String): Set[String] = { val (Seq(tempSrcFile), analysisCallback) = compileSrcs(src) analysisCallback.usedNames(tempSrcFile) @@ -66,7 +75,7 @@ class ScalaCompilerForUnitTesting(nameHashing: Boolean = false) { def extractDependenciesFromSrcs(srcs: List[Map[Symbol, String]]): ExtractedSourceDependencies = { val rawGroupedSrcs = srcs.map(_.values.toList) val symbols = srcs.flatMap(_.keys) - val (tempSrcFiles, testCallback) = compileSrcs(rawGroupedSrcs) + val (tempSrcFiles, testCallback) = compileSrcs(rawGroupedSrcs, reuseCompilerInstance = true) val fileToSymbol = (tempSrcFiles zip symbols).toMap val memberRefFileDeps = testCallback.sourceDependencies collect { @@ -109,19 +118,31 @@ class ScalaCompilerForUnitTesting(nameHashing: Boolean = false) { * useful to compile macros, which cannot be used in the same compilation run that * defines them. * + * The `reuseCompilerInstance` parameter controls whether the same Scala compiler instance + * is reused between compiling source groups. Separate compiler instances can be used to + * test stability of API representation (with respect to pickling) or to test handling of + * binary dependencies. + * * The sequence of temporary files corresponding to passed snippets and analysis * callback is returned as a result. */ - private def compileSrcs(groupedSrcs: List[List[String]]): (Seq[File], TestCallback) = { - withTemporaryDirectory { temp => - val analysisCallback = new TestCallback(nameHashing) + private def compileSrcs(groupedSrcs: List[List[String]], + reuseCompilerInstance: Boolean): (Seq[File], TestCallback) = { + // withTemporaryDirectory { temp => + { + val temp = createTemporaryDirectory + val analysisCallback = new TestCallback(nameHashing, includeSynthToNameHashing) val classesDir = new File(temp, "classes") classesDir.mkdir() - // val (compiler, ctx) = prepareCompiler(classesDir, analysisCallback, classesDir.toString) + lazy val commonCompilerInstanceAndCtx = prepareCompiler(classesDir, analysisCallback, classesDir.toString) val files = for ((compilationUnit, unitId) <- groupedSrcs.zipWithIndex) yield { - val (compiler, ctx) = prepareCompiler(classesDir, analysisCallback, classesDir.toString) + // use a separate instance of the compiler for each group of sources to + // have an ability to test for bugs in instability between source and pickled + // representation of types + val (compiler, ctx) = if (reuseCompilerInstance) commonCompilerInstanceAndCtx else + prepareCompiler(classesDir, analysisCallback, classesDir.toString) val run = compiler.newRun(ctx) val srcFiles = compilationUnit.toSeq.zipWithIndex map { case (src, i) => @@ -132,7 +153,7 @@ class ScalaCompilerForUnitTesting(nameHashing: Boolean = false) { run.compile(srcFilePaths) - srcFilePaths.foreach(f => new File(f).delete) + // srcFilePaths.foreach(f => new File(f).delete) srcFiles } (files.flatten.toSeq, analysisCallback) @@ -140,7 +161,7 @@ class ScalaCompilerForUnitTesting(nameHashing: Boolean = false) { } private def compileSrcs(srcs: String*): (Seq[File], TestCallback) = { - compileSrcs(List(srcs.toList)) + compileSrcs(List(srcs.toList), reuseCompilerInstance = true) } private def prepareSrcFile(baseDir: File, fileName: String, src: String): File = { @@ -151,10 +172,6 @@ class ScalaCompilerForUnitTesting(nameHashing: Boolean = false) { private def prepareCompiler(outputDir: File, analysisCallback: AnalysisCallback, classpath: String = ".") = { val args = Array.empty[String] - object output extends SingleOutput { - def outputDirectory: File = outputDir - override def toString = s"SingleOutput($outputDirectory)" - } import dotty.tools.dotc._ import dotty.tools.dotc.core.Contexts._ @@ -171,7 +188,7 @@ class ScalaCompilerForUnitTesting(nameHashing: Boolean = false) { } } val ctx = (new ContextBase).initialCtx.fresh.setSbtCallback(analysisCallback) - driver.getCompiler(Array("-classpath", classpath, "-usejavacp"), ctx) + driver.getCompiler(Array("-classpath", classpath, "-usejavacp", "-d", outputDir.getAbsolutePath), ctx) } private object ConsoleReporter extends Reporter { diff --git a/sbt-bridge/test/xsbti/TestCallback.scala b/sbt-bridge/test/xsbti/TestCallback.scala index b849e1a80..99c8d963d 100644 --- a/sbt-bridge/test/xsbti/TestCallback.scala +++ b/sbt-bridge/test/xsbti/TestCallback.scala @@ -6,7 +6,7 @@ import scala.collection.mutable.ArrayBuffer import xsbti.api.SourceAPI import xsbti.DependencyContext._ -class TestCallback(override val nameHashing: Boolean = false) extends AnalysisCallback +class TestCallback(override val nameHashing: Boolean, override val includeSynthToNameHashing: Boolean) extends AnalysisCallback { val sourceDependencies = new ArrayBuffer[(File, File, DependencyContext)] val binaryDependencies = new ArrayBuffer[(File, String, File, DependencyContext)] -- cgit v1.2.3