Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Scala 3 to MiMa's version matrix #587

Merged
merged 1 commit into from
Dec 18, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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... ¯\_(ツ)_/¯
dwijnand marked this conversation as resolved.
Show resolved Hide resolved
# https://github.com/lightbend/mima/issues/590