Skip to content

Commit

Permalink
Merge pull request #587 from dwijnand/scala3
Browse files Browse the repository at this point in the history
Add Scala 3 to MiMa's version matrix
  • Loading branch information
dwijnand committed Dec 18, 2020
2 parents 0abf389 + a506548 commit 3e9f383
Show file tree
Hide file tree
Showing 43 changed files with 214 additions and 84 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ jobs:
- { name: testFunctional 2.11, script: sbt "functional-tests/runMain com.typesafe.tools.mima.lib.UnitTests -211" }
- { name: testFunctional 2.12, script: sbt "functional-tests/runMain com.typesafe.tools.mima.lib.UnitTests -212" }
- { name: testFunctional 2.13, script: sbt "functional-tests/runMain com.typesafe.tools.mima.lib.UnitTests -213" }
- { name: testFunctional 3, script: sbt "functional-tests/runMain com.typesafe.tools.mima.lib.UnitTests -3" }
- { name: scripted 1/2, script: sbt "scripted sbt-mima-plugin/*1of2" }
- { name: scripted 2/2, script: sbt "scripted sbt-mima-plugin/*2of2" }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ private[analyze] object MethodChecker {

/** Analyze incompatibilities that may derive from new methods in `newclazz`. */
private def checkNew(oldclazz: ClassInfo, newclazz: ClassInfo): List[Problem] = {
(if (newclazz.isClass) Nil else checkEmulatedConcreteMethodsProblems(oldclazz, newclazz)) :::
checkDeferredMethodsProblems(oldclazz, newclazz) :::
checkInheritedNewAbstractMethodProblems(oldclazz, newclazz)
val problems1 = if (newclazz.isClass) Nil else checkEmulatedConcreteMethodsProblems(oldclazz, newclazz)
val problems2 = checkDeferredMethodsProblems(oldclazz, newclazz)
val problems3 = checkInheritedNewAbstractMethodProblems(oldclazz, newclazz)
problems1 ::: problems2 ::: problems3
}

private def checkExisting1(oldmeth: MethodInfo, newclazz: ClassInfo): Option[Problem] = {
Expand Down Expand Up @@ -138,11 +139,9 @@ private[analyze] object MethodChecker {
for {
newmeth <- newclazz.deferredMethods.iterator
problem <- oldclazz.lookupMethods(newmeth).find(_.descriptor == newmeth.descriptor) match {
case None => Some(ReversedMissingMethodProblem(newmeth))
case Some(oldmeth) =>
if (newclazz.isClass && oldmeth.isConcrete)
Some(ReversedAbstractMethodProblem(newmeth))
else None
case None => Some(ReversedMissingMethodProblem(newmeth))
case Some(oldmeth) if newclazz.isClass && oldmeth.isConcrete => Some(ReversedAbstractMethodProblem(newmeth))
case Some(_) => None
}
} yield problem
}.toList
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,15 @@ object AppRunTest {
def testAppRun1(testCase: TestCase, v1: Directory, v2: Directory, oracleFile: Path): Try[Unit] = for {
() <- testCase.compileBoth
pending = testCase.versionedFile("testAppRun.pending").exists
insane = testCase.versionedFile("testAppRun.insane").exists
expectOk = testCase.blankFile(testCase.versionedFile(oracleFile))
//() <- testCase.compileApp(v2) // compile app with v2
//() <- testCase.runMain(v2) // sanity check 1: run app with v2
() <- testCase.compileApp(v1) // recompile app with v1
() <- testCase.runMain(v1) // sanity check 2: run app with v1
() <- testCase.runMain(v1) match { // sanity check 2: run app with v1
case Failure(t) if !insane => Failure(new Exception("Sanity runMain check failed", t, true, false) {})
case _ => Success(())
}
() <- testCase.runMain(v2) match { // test: run app, compiled with v1, with v2
case Failure(t) if !pending && expectOk => Failure(t)
case Success(()) if !pending && !expectOk => Failure(new Exception("expected running App to fail"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,27 +31,24 @@ object CollectProblemsTest {
case Forwards => "other"
}

// diff between the oracle and the collected problems
val unexpected = problems.filter(p => !expected.contains(p.description(affectedVersion)))
val unreported = expected.diff(problems.map(_.description(affectedVersion)))
val reported = problems.map(_.description(affectedVersion))

val msg = new StringBuilder("\n")
def pp(start: String, lines: List[String]) = {
if (lines.isEmpty) ()
else lines.sorted.distinct.addString(msg, s"$start (${lines.size}):\n - ", "\n - ", "\n")
}
pp("The following problem(s) were expected but not reported", unreported)
pp("The following problem(s) were reported but not expected", unexpected.map(_.description(affectedVersion)))
pp("Filter with:", unexpected.flatMap(_.howToFilter))
pp("Or filter with:", unexpected.flatMap(p => p.matchName.map { matchName =>
s"{ matchName=$dq$matchName$dq , problemName=${p.getClass.getSimpleName} }"
}))
def pp(start: String, lines: List[String]) =
if (lines.nonEmpty) {
msg.append(s"$start (${lines.size}):")
lines.sorted.distinct.map("\n - " + _).foreach(msg.append(_))
msg.append("\n")
}

pp("The following problem(s) were expected but not reported", expected.diff(reported))
pp("The following problem(s) were reported but not expected", reported.diff(expected))

msg.mkString match {
case "\n" => Success(())
case msg => Failure(new Exception(msg))
case msg =>
Console.err.println(msg)
Failure(new Exception("CollectProblemsTest failure"))
}
}

private final val dq = '"' // scala/bug#6476 -.-
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import scala.util.{ Failure, Success, Try }
import coursier._

final class ScalaCompiler(val version: String) {
val jars = Coursier.fetch(Dependency(mod"org.scala-lang:scala-compiler", version))
val isScala3 = version.startsWith("3.")

val name = if (isScala3) ModuleName(s"scala3-compiler_$version") else name"scala-compiler"
val jars = Coursier.fetch(Dependency(Module(org"org.scala-lang", name), version))

val classLoader = new URLClassLoader(jars.toArray.map(_.toURI.toURL), parentClassLoader())

Expand All @@ -17,16 +20,18 @@ final class ScalaCompiler(val version: String) {

def compile(args: Seq[String]): Try[Unit] = {
import scala.language.reflectiveCalls
val cls = classLoader.loadClass("scala.tools.nsc.Main$")
val clsName = if (isScala3) "dotty.tools.dotc.Main$" else "scala.tools.nsc.Main$"
val cls = classLoader.loadClass(clsName)
type Main = { def process(args: Array[String]): Any; def reporter: Reporter }
type Reporter = { def hasErrors: Boolean }
val m = cls.getField("MODULE$").get(null).asInstanceOf[Main]
Try {
val success = m.process(args.toArray) match {
case b: Boolean => b
case null => !m.reporter.hasErrors // nsc 2.11
case x => !x.asInstanceOf[Reporter].hasErrors // dotc
}
if (success) Success(()) else Failure(new Exception("scalac failed"))
}
}.flatten
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,19 @@ object Test {
def pass = s"${Console.GREEN}\u2713${Console.RESET}" // check mark (green)
def fail = s"${Console.RED}\u2717${Console.RESET}" // cross mark (red)

def testAll(tests: List[Test]): Try[Unit] = {
tests.iterator.map(_.run()).foldLeft(Try(())) {
case (res @ Failure(e1), Failure(e2)) => e1.addSuppressed(e2); res
case (res @ Failure(_), _) => res
case (_, res) => res
def testAll(tests: List[Test1]): Try[Unit] = {
val (successes, failures) = tests.map(t => t -> Test.run1(t.label, t.action)).partition(_._2.isSuccess)
println(s"${tests.size} tests, ${successes.size} successes, ${failures.size} failures")
if (failures.nonEmpty) {
val failureNames = failures.map { case ((t1, _)) => t1.name }
println("Failures:")
failureNames.foreach(name => println(s"* $name"))
println(s"functional-tests/Test/run ${failureNames.mkString(" ")}")
}
failures.foldLeft(Try(())) {
case (res @ Failure(e1), (_, Failure(e2))) => e1.addSuppressed(e2); res
case (res @ Failure(_), _) => res
case (_, (_, res)) => res
}
}

Expand All @@ -24,17 +32,6 @@ object Test {
case res @ Failure(ex) => println(s"- $fail $label: $ex"); res
}
}

implicit class TestOps(private val t: Test) extends AnyVal {
def tests: List[Test1] = t match {
case t1: Test1 => List(t1)
case Tests(tests) => tests
}

def munitTests: List[GenericTest[Unit]] = for {
test <- t.tests
} yield new GenericTest(test.label, () => test.unsafeRunTest(), Set.empty, Location.empty)
}
}

sealed trait Test {
Expand All @@ -51,5 +48,22 @@ sealed trait Test {
}
}

object Test1 {
implicit class Ops(private val t: Test1) extends AnyVal {
def name: String = t.label.indexOf(" / ") match {
case -1 => t.label
case idx => t.label.drop(idx + 3)
}
}
}

object Tests {
implicit class Ops(private val t: Tests) extends AnyVal {
def munitTests: List[GenericTest[Unit]] = for {
test <- t.tests
} yield new GenericTest(test.label, () => test.unsafeRunTest(), Set.empty, Location.empty)
}
}

case class Test1(label: String, action: () => Try[Unit]) extends Test
case class Tests(tests: List[Test1]) extends Test
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import java.io.{ ByteArrayOutputStream, PrintStream }
import java.net.{ URI, URLClassLoader }
import javax.tools._

import scala.annotation.tailrec
import scala.collection.JavaConverters._
import scala.collection.mutable
import scala.reflect.internal.util.BatchSourceFile
Expand All @@ -14,7 +15,7 @@ import com.typesafe.tools.mima.core.ClassPath

final class TestCase(val baseDir: Directory, val scalaCompiler: ScalaCompiler, val javaCompiler: JavaCompiler) {
def name = baseDir.name
def scalaBinaryVersion = scalaCompiler.version.take(4)
def scalaBinaryVersion = if (scalaCompiler.isScala3) "3" else scalaCompiler.version.take(4)
def scalaJars = scalaCompiler.jars

val srcV1 = (baseDir / "v1").toDirectory
Expand Down Expand Up @@ -42,7 +43,7 @@ final class TestCase(val baseDir: Directory, val scalaCompiler: ScalaCompiler, v
val sourceFiles = lsSrcs(srcDir)
if (sourceFiles.forall(_.isJava)) return Success(())
val bootcp = ClassPath.join(scalaJars.map(_.getPath))
val cpOpt = if (cp.isEmpty) Nil else List("-cp", ClassPath.join(cp.map(_.path)))
val cpOpt = if (cp.isEmpty) Nil else List("-classpath", ClassPath.join(cp.map(_.path)))
val paths = sourceFiles.map(_.path)
val args = "-bootclasspath" :: bootcp :: cpOpt ::: "-d" :: s"$out" :: paths
scalaCompiler.compile(args)
Expand Down Expand Up @@ -78,7 +79,16 @@ final class TestCase(val baseDir: Directory, val scalaCompiler: ScalaCompiler, v
System.setErr(printStream)
Console.withErr(printStream) {
Console.withOut(printStream) {
Try(meth.invoke(null, new Array[String](0)): Unit)
try {
meth.invoke(null, new Array[String](0))
Success(())
} catch {
case e: VirtualMachineError => throw e
case e: ThreadDeath => throw e
case e: InterruptedException => throw e
case e: scala.util.control.ControlThrowable => throw e // don't rethrow LinkageError
case e: Throwable => Failure(rootCause(e))
}
}
}
} finally {
Expand All @@ -98,9 +108,11 @@ final class TestCase(val baseDir: Directory, val scalaCompiler: ScalaCompiler, v
val p = baseDir.resolve(path).toFile
val p211 = (p.parent / (s"${p.stripExtension}-2.11")).addExtension(p.extension).toFile
val p212 = (p.parent / (s"${p.stripExtension}-2.12")).addExtension(p.extension).toFile
val p3 = (p.parent / (s"${p.stripExtension}-3" )).addExtension(p.extension).toFile
scalaBinaryVersion match {
case "2.11" => if (p211.exists) p211 else if (p212.exists) p212 else p
case "2.12" => if (p212.exists) p212 else p
case "3" => if (p3.exists) p3 else p
case _ => p
}
}
Expand All @@ -112,5 +124,15 @@ final class TestCase(val baseDir: Directory, val scalaCompiler: ScalaCompiler, v
()
}

@tailrec private def rootCause(x: Throwable): Throwable = x match {
case _: ExceptionInInitializerError |
_: java.lang.reflect.InvocationTargetException |
_: java.lang.reflect.UndeclaredThrowableException |
_: java.util.concurrent.ExecutionException
if x.getCause != null =>
rootCause(x.getCause)
case _ => x
}

override def toString = s"TestCase(baseDir=${baseDir.name}, scalaVersion=${scalaCompiler.version})"
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ import scala.util.{ Properties => StdLibProps }

object TestCli {
val scala211 = "2.11.12"
val scala212 = "2.12.11"
val scala213 = "2.13.2"
val scala212 = "2.12.12"
val scala213 = "2.13.4"
val scala3 = "3.0.0-M3"
val hostScalaVersion = StdLibProps.scalaPropOrNone("maven.version.number").get
val allScalaVersions = List(scala211, scala212, scala213)
val allScalaVersions = List(scala211, scala212, scala213, scala3)
val testsDir = Directory("functional-tests/src/test")

def argsToTests(args: List[String], runTestCase: TestCase => Try[Unit]): Tests =
Expand All @@ -24,7 +25,7 @@ object TestCli {
def testCaseToTest1(tc: TestCase, runTestCase: TestCase => Try[Unit]): Test1 =
Test(s"${tc.scalaBinaryVersion} / ${tc.name}", runTestCase(tc))

def fromArgs(args: List[String]): List[TestCase] = fromConf(go(args, Conf(Nil, Nil)))
def fromArgs(args: List[String]): List[TestCase] = fromConf(readArgs(args, Conf(Nil, Nil)))

@tailrec def postProcessConf(conf: Conf): Conf = conf match {
case Conf(Nil, _) => postProcessConf(conf.copy(scalaVersions = List(hostScalaVersion)))
Expand All @@ -49,13 +50,14 @@ object TestCli {

final case class Conf(scalaVersions: List[String], dirs: List[Directory])

@tailrec private def go(argv: List[String], conf: Conf): Conf = argv match {
case "-213" :: xs => go(xs, conf.copy(scalaVersions = scala213 :: conf.scalaVersions))
case "-212" :: xs => go(xs, conf.copy(scalaVersions = scala212 :: conf.scalaVersions))
case "-211" :: xs => go(xs, conf.copy(scalaVersions = scala211 :: conf.scalaVersions))
case "--scala-version" :: sv :: xs => go(xs, conf.copy(scalaVersions = sv :: conf.scalaVersions))
case "--cross" :: xs => go(xs, conf.copy(scalaVersions = List(scala211, scala212, scala213)))
case s :: xs => go(xs, conf.copy(dirs = testDirs(s) ::: conf.dirs))
@tailrec private def readArgs(args: List[String], conf: Conf): Conf = args match {
case "-3" :: xs => readArgs(xs, conf.copy(scalaVersions = scala3 :: conf.scalaVersions))
case "-213" :: xs => readArgs(xs, conf.copy(scalaVersions = scala213 :: conf.scalaVersions))
case "-212" :: xs => readArgs(xs, conf.copy(scalaVersions = scala212 :: conf.scalaVersions))
case "-211" :: xs => readArgs(xs, conf.copy(scalaVersions = scala211 :: conf.scalaVersions))
case "--scala-version" :: sv :: xs => readArgs(xs, conf.copy(scalaVersions = sv :: conf.scalaVersions))
case "--cross" :: xs => readArgs(xs, conf.copy(scalaVersions = allScalaVersions))
case s :: xs => readArgs(xs, conf.copy(dirs = testDirs(s) ::: conf.dirs))
case Nil => conf
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
object App {
def main(args: Array[String]): Unit = {
println(new A { def foo = () }.foo)
object a extends A { def foo() = () }
println(a.foo())
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
abstract class A extends B {
def foo: Unit
def foo(): Unit
}

trait B {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
object App {
def main(args: Array[String]): Unit = {
println(new A { def baz = 2 }.baz)
object a extends A { def baz = 2 }
println(a.baz)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class A was concrete; is declared abstract in new version
method apply()A in object A does not have a correspondent in new version
static method apply()A in class A does not have a correspondent in new version
the type hierarchy of object A is different in new version. Missing types {scala.deriving.Mirror$Product}
method fromProduct(scala.Product)A in object A does not have a correspondent in new version
static method fromProduct(scala.Product)A in class A does not have a correspondent in new version
# the last 2 are not present in Scala 2
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
object App {
def main(args: Array[String]): Unit = {
println(new A)
println(A())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# In Scala 2 none of these changes are present. Positive progress IMO.
method apply()A in object A does not have a correspondent in new version
static method apply()A in class A does not have a correspondent in new version
method copy()A in class A does not have a correspondent in new version
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
object App {
def main(args: Array[String]): Unit = {
println(new A { def baz = 2 }.baz)
object a extends A { def baz = 2 }
println(a.baz)
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
object App {
def main(args: Array[String]): Unit = {
val result: String = new OptionPane(("foo", "bar")).show
val result: String = new OptionPane(("foo", "bar")).show()
println("result: " + result)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
method source()scala.Tuple2 in class OptionPane has a different generic signature in new version, where it is ()Lscala/Tuple2<LMyPane<TA;>;Ljava/lang/String;>; rather than ()Lscala/Tuple2<Ljava/lang/String;Ljava/lang/String;>;. See https://github.com/lightbend/mima#incompatiblesignatureproblem
method show()java.lang.String in class OptionPane does not have a correspondent in new version
method this(scala.Tuple2)Unit in class OptionPane has a different generic signature in new version, where it is <A:Ljava/lang/Object;>(Lscala/Tuple2<LMyPane<TA;>;Ljava/lang/String;>;)V rather than (Lscala/Tuple2<Ljava/lang/String;Ljava/lang/String;>;)V. See https://github.com/lightbend/mima#incompatiblesignatureproblem
# In Scala 2 it's:
# ... where it is (Lscala/Tuple2<LMyPane<TA;>;Ljava/lang/String;>;)V rather than ...
# note there's no new type parameter
# https://github.com/lampepfl/dotty/issues/10834
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
in new version there is abstract method foo()Int in class B, which does not have a correspondent
# what's missing is:
# abstract method foo()Int in class B does not have a correspondent in new version
# which is a `DirectAbstractMethodProblem`, rather than the above `ReversedAbstractMethodProblem`
# not sure exactly what that means... ¯\_(ツ)_/¯
# https://github.com/lightbend/mima/issues/590
Loading

0 comments on commit 3e9f383

Please sign in to comment.