Skip to content

Commit

Permalink
Add support for versions with less segments (lightbend-labs#212)
Browse files Browse the repository at this point in the history
  • Loading branch information
2m committed May 22, 2018
1 parent bd45dde commit de4719a
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 50 deletions.
1 change: 1 addition & 0 deletions project/Build.scala
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -65,7 +63,7 @@ object MimaPlugin extends AutoPlugin {
currentClassfiles,
cp,
checkDirection,
taskStreams
log
)
SbtMima.reportModuleErrors(
moduleId,
Expand All @@ -74,7 +72,7 @@ object MimaPlugin extends AutoPlugin {
binaryIssueFilters,
backwardIssueFilters,
forwardIssueFilters,
taskStreams,
log,
projectName)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,42 +24,29 @@ 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.
*/
Expand All @@ -70,48 +57,52 @@ 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 { log.info }
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(epoch, major, minor) = version
val ModuleVersion = """(\d+)\.?(\d+)?\.?(.*)?""".r
val (epoch, major, minor) = version match {
case ModuleVersion(e, m, mi) => (e, m, mi)
case ModuleVersion(e, m, null) => (e, m, "0")
case ModuleVersion(e, null, null) => (e, "0", "0")
}
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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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 = ()
}

final val NoMemberInfo = new MemberInfo(NoClass, "", 0, "")

final val AllMatchingFilter = (_: Problem) => false
}

0 comments on commit de4719a

Please sign in to comment.