diff --git a/core/src/main/scala/com/typesafe/tools/mima/core/util/log/ConsoleLogging.scala b/core/src/main/scala/com/typesafe/tools/mima/core/util/log/ConsoleLogging.scala index 6a759366..ede41cbc 100644 --- a/core/src/main/scala/com/typesafe/tools/mima/core/util/log/ConsoleLogging.scala +++ b/core/src/main/scala/com/typesafe/tools/mima/core/util/log/ConsoleLogging.scala @@ -5,4 +5,6 @@ import com.typesafe.tools.mima.core.Config object ConsoleLogging extends Logging { def info(str: String) = if (Config.verbose) println(str) def debugLog(str: String) = if (Config.debug) println(str) -} \ No newline at end of file + def warn(str: String) = println(str) + def error(str: String) = Console.err.println(str) +} diff --git a/core/src/main/scala/com/typesafe/tools/mima/core/util/log/Logging.scala b/core/src/main/scala/com/typesafe/tools/mima/core/util/log/Logging.scala index 8d3f9452..3f2da64b 100644 --- a/core/src/main/scala/com/typesafe/tools/mima/core/util/log/Logging.scala +++ b/core/src/main/scala/com/typesafe/tools/mima/core/util/log/Logging.scala @@ -3,4 +3,6 @@ package com.typesafe.tools.mima.core.util.log trait Logging { def info(str: String): Unit def debugLog(str: String): Unit -} \ No newline at end of file + def warn(str: String): Unit + def error(str: String): Unit +} diff --git a/project/Build.scala b/project/Build.scala index 31bb0e4d..7ff199d1 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -134,6 +134,16 @@ object MimaBuild { ), name := buildName + "-core") settings(sonatypePublishSettings:_*) + settings( + mimaBinaryIssueFilters ++= { + import com.typesafe.tools.mima.core._ + Seq( + // Add support for versions with less segments (#212) + ProblemFilters.exclude[ReversedMissingMethodProblem]("com.typesafe.tools.mima.core.util.log.Logging.warn") + ProblemFilters.exclude[ReversedMissingMethodProblem]("com.typesafe.tools.mima.core.util.log.Logging.error") + ) + } + ) ) val myAssemblySettings: Seq[Setting[_]] = (assemblySettings: Seq[Setting[_]]) ++ Seq( @@ -190,6 +200,7 @@ object MimaBuild { (sbtBinaryVersion in pluginCrossBuild).value, (scalaBinaryVersion in update).value ), + libraryDependencies += scalatest, scriptedLaunchOpts := scriptedLaunchOpts.value :+ "-Dplugin.version=" + version.value, scriptedBufferLog := false, // Scripted locally publishes sbt plugin and then runs test projects with locally published version. @@ -212,7 +223,13 @@ object MimaBuild { ProblemFilters.exclude[MissingClassProblem]("sbt.librarymanagement.UpdateConfiguration$"), ProblemFilters.exclude[MissingClassProblem]("sbt.librarymanagement.DependencyResolution"), ProblemFilters.exclude[MissingClassProblem]("sbt.librarymanagement.ivy.IvyDependencyResolution"), - ProblemFilters.exclude[MissingClassProblem]("sbt.librarymanagement.ivy.IvyDependencyResolution$") + ProblemFilters.exclude[MissingClassProblem]("sbt.librarymanagement.ivy.IvyDependencyResolution$"), + + // Add support for versions with less segments (#212) + // All of these are MiMa sbtplugin internals and can be safely filtered + ProblemFilters.exclude[IncompatibleMethTypeProblem]("com.typesafe.tools.mima.plugin.SbtMima.runMima"), + ProblemFilters.exclude[DirectMissingMethodProblem]("com.typesafe.tools.mima.plugin.SbtMima.reportErrors"), + ProblemFilters.exclude[IncompatibleMethTypeProblem]("com.typesafe.tools.mima.plugin.SbtMima.reportModuleErrors") ) } ) diff --git a/sbtplugin/src/main/scala/com/typesafe/tools/mima/plugin/MimaPlugin.scala b/sbtplugin/src/main/scala/com/typesafe/tools/mima/plugin/MimaPlugin.scala index df82c79d..ef9de915 100644 --- a/sbtplugin/src/main/scala/com/typesafe/tools/mima/plugin/MimaPlugin.scala +++ b/sbtplugin/src/main/scala/com/typesafe/tools/mima/plugin/MimaPlugin.scala @@ -23,8 +23,7 @@ object MimaPlugin extends AutoPlugin { mimaBackwardIssueFilters := SbtMima.issueFiltersFromFiles(mimaFiltersDirectory.value, "\\.(?:backward[s]?|both)\\.excludes".r, streams.value), mimaForwardIssueFilters := SbtMima.issueFiltersFromFiles(mimaFiltersDirectory.value, "\\.(?:forward[s]?|both)\\.excludes".r, streams.value), mimaFindBinaryIssues := { - val taskStreams = streams.value - val log = taskStreams.log + val log = new SbtLogger(streams.value) val projectName = name.value val previousClassfiles = mimaPreviousClassfiles.value val currentClassfiles = mimaCurrentClassfiles.value @@ -37,14 +36,13 @@ object MimaPlugin extends AutoPlugin { else { previousClassfiles.map { case (moduleId, file) => - val problems = SbtMima.runMima(file, currentClassfiles, cp, checkDirection, taskStreams) + val problems = SbtMima.runMima(file, currentClassfiles, cp, checkDirection, log) (moduleId, (problems._1, problems._2)) } } }, mimaReportBinaryIssues := { - val taskStreams = streams.value - val log = taskStreams.log + val log = new SbtLogger(streams.value) val projectName = name.value val currentClassfiles = mimaCurrentClassfiles.value val cp = (fullClasspath in mimaFindBinaryIssues).value @@ -65,7 +63,7 @@ object MimaPlugin extends AutoPlugin { currentClassfiles, cp, checkDirection, - taskStreams + log ) SbtMima.reportModuleErrors( moduleId, @@ -74,7 +72,7 @@ object MimaPlugin extends AutoPlugin { binaryIssueFilters, backwardIssueFilters, forwardIssueFilters, - taskStreams, + log, projectName) } } diff --git a/sbtplugin/src/main/scala/com/typesafe/tools/mima/plugin/SbtMima.scala b/sbtplugin/src/main/scala/com/typesafe/tools/mima/plugin/SbtMima.scala index 3db11112..41ab2da0 100644 --- a/sbtplugin/src/main/scala/com/typesafe/tools/mima/plugin/SbtMima.scala +++ b/sbtplugin/src/main/scala/com/typesafe/tools/mima/plugin/SbtMima.scala @@ -18,48 +18,37 @@ class SbtLogger(s: TaskStreams) extends Logging { // Mima is prety chatty def info(str: String): Unit = s.log.debug(str) def debugLog(str: String): Unit = s.log.debug(str) + def warn(str: String): Unit = s.log.warn(str) + def error(str: String): Unit = s.log.error(str) } object SbtMima { val x = sbt.Keys.fullClasspath /** Creates a new MiMaLib object to run analysis. */ - private def makeMima(cp: sbt.Keys.Classpath, s: TaskStreams): lib.MiMaLib = { + private def makeMima(cp: sbt.Keys.Classpath, log: Logging): lib.MiMaLib = { // TODO: Fix MiMa so we don't have to hack this bit in. core.Config.setup("sbt-mima-plugin", Array.empty) val cpstring = cp map (_.data.getAbsolutePath()) mkString System.getProperty("path.separator") val classpath = com.typesafe.tools.mima.core.reporterClassPath(cpstring) - new lib.MiMaLib(classpath, new SbtLogger(s)) + new lib.MiMaLib(classpath, log) } /** Runs MiMa and returns a two lists of potential binary incompatibilities, the first for backward compatibility checking, and the second for forward checking. */ def runMima(prev: File, curr: File, cp: sbt.Keys.Classpath, - dir: String, s: TaskStreams): (List[core.Problem], List[core.Problem]) = { + dir: String, log: Logging): (List[core.Problem], List[core.Problem]) = { // MiMaLib collects problems to a mutable buffer, therefore we need a new instance every time (dir match { - case "backward" | "backwards" | "both" => makeMima(cp, s).collectProblems(prev.getAbsolutePath, curr.getAbsolutePath) + case "backward" | "backwards" | "both" => makeMima(cp, log).collectProblems(prev.getAbsolutePath, curr.getAbsolutePath) case _ => Nil }, dir match { - case "forward" | "forwards" | "both" => makeMima(cp, s).collectProblems(curr.getAbsolutePath, prev.getAbsolutePath) + case "forward" | "forwards" | "both" => makeMima(cp, log).collectProblems(curr.getAbsolutePath, prev.getAbsolutePath) case _ => Nil }) } - /** Reports binary compatibility errors. - * @param failOnProblem if true, fails the build on binary compatibility errors. - */ - def reportErrors(problemsInFiles: Map[ModuleID, (List[core.Problem], List[core.Problem])], - failOnProblem: Boolean, - filters: Seq[core.ProblemFilter], - backwardFilters: Map[String, Seq[core.ProblemFilter]], - forwardFilters: Map[String, Seq[core.ProblemFilter]], - s: TaskStreams, projectName: String): Unit = - problemsInFiles foreach { case (module, (backward, forward)) => - reportModuleErrors(module, backward, forward, failOnProblem, filters, backwardFilters, forwardFilters, s, projectName) - } - /** Reports binary compatibility errors for a module. * @param failOnProblem if true, fails the build on binary compatibility errors. */ @@ -70,48 +59,51 @@ object SbtMima { filters: Seq[core.ProblemFilter], backwardFilters: Map[String, Seq[core.ProblemFilter]], forwardFilters: Map[String, Seq[core.ProblemFilter]], - s: TaskStreams, projectName: String): Unit = { + log: Logging, projectName: String): Unit = { // filters * found is n-squared, it's fixable in principle by special-casing known // filter types or something, not worth it most likely... + val backErrors = backward filter isReported(module, filters, backwardFilters)(log, projectName) + val forwErrors = forward filter isReported(module, filters, forwardFilters)(log, projectName) + + val filteredCount = backward.size + forward.size - backErrors.size - forwErrors.size + val filteredNote = if (filteredCount > 0) " (filtered " + filteredCount + ")" else "" + + // TODO - Line wrapping an other magikz + def prettyPrint(p: core.Problem, affected: String): String = { + " * " + p.description(affected) + p.howToFilter.map("\n filter with: " + _).getOrElse("") + } + + log.info(s"$projectName: found ${backErrors.size+forwErrors.size} potential binary incompatibilities while checking against $module $filteredNote") + ((backErrors map {p: core.Problem => prettyPrint(p, "current")}) ++ + (forwErrors map {p: core.Problem => prettyPrint(p, "other")})) foreach { p => + if (failOnProblem) log.error(p) + else log.warn(p) + } + if (failOnProblem && (backErrors.nonEmpty || forwErrors.nonEmpty)) sys.error(projectName + ": Binary compatibility check failed!") + } + + private[mima] def isReported(module: ModuleID, filters: Seq[core.ProblemFilter], versionedFilters: Map[String, Seq[core.ProblemFilter]])(log: Logging, projectName: String)(problem: core.Problem) = { + // version string "x.y.z" is converted to an Int tuple (x, y, z) for comparison val versionOrdering = Ordering[(Int, Int, Int)].on { version: String => - val ModuleVersion = """(\d+)\.(\d+)\.(.*)""".r + val ModuleVersion = """(\d+)\.?(\d+)?\.?(.*)?""".r val ModuleVersion(epoch, major, minor) = version val toNumeric = (revision: String) => Try(revision.replace("x", Short.MaxValue.toString).filter(_.isDigit).toInt).getOrElse(0) (toNumeric(epoch), toNumeric(major), toNumeric(minor)) } - def isReported(module: ModuleID, verionedFilters: Map[String, Seq[core.ProblemFilter]])(problem: core.Problem) = (verionedFilters.collect { + (versionedFilters.collect { // get all filters that apply to given module version or any version after it - case f @ (version, filters) if versionOrdering.gteq(version, module.revision) => filters + case f @ (version, versionFilters) if versionOrdering.gteq(version, module.revision) => versionFilters }.flatten ++ filters).forall { f => if (f(problem)) { true } else { - s.log.debug(projectName + ": filtered out: " + problem.description + "\n filtered by: " + f) + log.debugLog(projectName + ": filtered out: " + problem.description + "\n filtered by: " + f) false } } - - val backErrors = backward filter isReported(module, backwardFilters) - val forwErrors = forward filter isReported(module, forwardFilters) - - val filteredCount = backward.size + forward.size - backErrors.size - forwErrors.size - val filteredNote = if (filteredCount > 0) " (filtered " + filteredCount + ")" else "" - - // TODO - Line wrapping an other magikz - def prettyPrint(p: core.Problem, affected: String): String = { - " * " + p.description(affected) + p.howToFilter.map("\n filter with: " + _).getOrElse("") - } - - s.log.info(s"$projectName: found ${backErrors.size+forwErrors.size} potential binary incompatibilities while checking against $module $filteredNote") - ((backErrors map {p: core.Problem => prettyPrint(p, "current")}) ++ - (forwErrors map {p: core.Problem => prettyPrint(p, "other")})) foreach { p => - if (failOnProblem) s.log.error(p) - else s.log.warn(p) - } - if (failOnProblem && (backErrors.nonEmpty || forwErrors.nonEmpty)) sys.error(projectName + ": Binary compatibility check failed!") } /** Resolves an artifact representing the previous abstract binary interface diff --git a/sbtplugin/src/test/scala/com/typesafe/tools/mima/plugin/ProblemReportingSpec.scala b/sbtplugin/src/test/scala/com/typesafe/tools/mima/plugin/ProblemReportingSpec.scala new file mode 100644 index 00000000..c3cedde4 --- /dev/null +++ b/sbtplugin/src/test/scala/com/typesafe/tools/mima/plugin/ProblemReportingSpec.scala @@ -0,0 +1,63 @@ +package com.typesafe.tools.mima.plugin + +import com.typesafe.tools.mima.core.util.log.Logging +import com.typesafe.tools.mima.core._ +import sbt._ +import org.scalatest.{Matchers, WordSpec} + +class ProblemReportingSpec extends WordSpec with Matchers { + import ProblemReportingSpec._ + + "problem" should { + "be reported when there are no filters" in { + isReported("0.0.1", Seq.empty) shouldBe true + } + + "not be reported when filtered out by general filters" in { + isReported("0.0.1", Seq(AllMatchingFilter)) shouldBe false + } + + "not be reported when filtered out by versioned filters" in { + isReported("0.0.1", Map("0.0.1" -> Seq(AllMatchingFilter))) shouldBe false + } + + "not be reported when filtered out by versioned wildcard filters" in { + isReported("0.0.1", Map("0.0.x" -> Seq(AllMatchingFilter))) shouldBe false + } + + "not be reported when filter version does not have patch segment" in { + isReported("0.1", Map("0.1" -> Seq(AllMatchingFilter))) shouldBe false + } + + "not be reported when filter version is only epoch" in { + isReported("1", Map("1" -> Seq(AllMatchingFilter))) shouldBe false + } + + "not be reported when filter version has less segments than module version" in { + isReported("0.1.0", Map("0.1" -> Seq(AllMatchingFilter))) shouldBe false + } + + "not be reported when filter version has more segments than module version" in { + isReported("0.1", Map("0.1.0" -> Seq(AllMatchingFilter))) shouldBe false + } + } + + private def isReported(moduleVersion: String, filters: Seq[ProblemFilter]) = + SbtMima.isReported("test" % "module" % moduleVersion, filters, Map.empty)(NoOpLogger, "test")(MissingFieldProblem(NoMemberInfo)) + private def isReported(moduleVersion: String, versionedFilters: Map[String, Seq[ProblemFilter]]) = + SbtMima.isReported("test" % "module" % moduleVersion, Seq.empty, versionedFilters)(NoOpLogger, "test")(MissingFieldProblem(NoMemberInfo)) + +} + +object ProblemReportingSpec { + final val NoOpLogger = new Logging { + override def info(str: String): Unit = () + override def debugLog(str: String): Unit = () + override def warn(str: String): Unit = () + override def error(str: String): Unit = () + } + + final val NoMemberInfo = new MemberInfo(NoClass, "", 0, "") + + final val AllMatchingFilter = (_: Problem) => false +}