From 2b8fa35b8da57f1ff9d3439c37881a7e07d06964 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Sun, 27 Jul 2014 12:26:12 -0400 Subject: [PATCH] Implements eviction warning stories. #1200 This implements all stories from https://github.com/sbt/sbt/wiki/User-Stories%3A-Conflict-Warning. When scalaVersion is no longer effective an eviction warning will display. Scala version was updated by one of library dependencies: * org.scala-lang:scala-library:2.10.2 -> 2.10.3 When there're suspected incompatibility in directly depended Java libraries, eviction warnings will display. There may be incompatibilities among your library dependencies. Here are some of the libraries that were evicted: * commons-io:commons-io:1.4 -> 2.4 When there's suspected incompatiblity in directly depended Scala libraries, eviction warnings will display. There may be incompatibilities among your library dependencies. Here are some of the libraries that were evicted: * com.typesafe.akka:akka-actor_2.10:2.1.4 -> 2.3.4 This also adds 'evicted' task, which displays more detailed eviction warnings. --- ivy/src/main/scala/sbt/EvictionWarning.scala | 188 +++++++++++++-- ivy/src/test/scala/BaseIvySpecification.scala | 51 ++++ ivy/src/test/scala/EvictionWarningSpec.scala | 224 ++++++++++++++++++ 3 files changed, 447 insertions(+), 16 deletions(-) create mode 100644 ivy/src/test/scala/BaseIvySpecification.scala create mode 100644 ivy/src/test/scala/EvictionWarningSpec.scala diff --git a/ivy/src/main/scala/sbt/EvictionWarning.scala b/ivy/src/main/scala/sbt/EvictionWarning.scala index 4d3baec98..bacebdee3 100644 --- a/ivy/src/main/scala/sbt/EvictionWarning.scala +++ b/ivy/src/main/scala/sbt/EvictionWarning.scala @@ -1,25 +1,120 @@ package sbt import collection.mutable +import Configurations.Compile -final class EvictionWarningOptions( - val configurations: Seq[String], - val level: Level.Value) { +final class EvictionWarningOptions private[sbt] ( + val configurations: Seq[Configuration], + val warnScalaVersionEviction: Boolean, + val warnDirectEvictions: Boolean, + val warnTransitiveEvictions: Boolean, + val showCallers: Boolean, + val guessCompatible: Function1[(ModuleID, Option[ModuleID], Option[IvyScala]), Boolean]) { + private[sbt] def configStrings = configurations map { _.name } + + def withConfigurations(configurations: Seq[Configuration]): EvictionWarningOptions = + copy(configurations = configurations) + def withWarnScalaVersionEviction(warnScalaVersionEviction: Boolean): EvictionWarningOptions = + copy(warnScalaVersionEviction = warnScalaVersionEviction) + def withWarnDirectEvictions(warnDirectEvictions: Boolean): EvictionWarningOptions = + copy(warnDirectEvictions = warnDirectEvictions) + def withWarnTransitiveEvictions(warnTransitiveEvictions: Boolean): EvictionWarningOptions = + copy(warnTransitiveEvictions = warnTransitiveEvictions) + def withShowCallers(showCallers: Boolean): EvictionWarningOptions = + copy(showCallers = showCallers) + def withGuessCompatible(guessCompatible: Function1[(ModuleID, Option[ModuleID], Option[IvyScala]), Boolean]): EvictionWarningOptions = + copy(guessCompatible = guessCompatible) + + private[sbt] def copy(configurations: Seq[Configuration] = configurations, + warnScalaVersionEviction: Boolean = warnScalaVersionEviction, + warnDirectEvictions: Boolean = warnDirectEvictions, + warnTransitiveEvictions: Boolean = warnTransitiveEvictions, + showCallers: Boolean = showCallers, + guessCompatible: Function1[(ModuleID, Option[ModuleID], Option[IvyScala]), Boolean] = guessCompatible): EvictionWarningOptions = + new EvictionWarningOptions(configurations = configurations, + warnScalaVersionEviction = warnScalaVersionEviction, + warnDirectEvictions = warnDirectEvictions, + warnTransitiveEvictions = warnTransitiveEvictions, + showCallers = showCallers, + guessCompatible = guessCompatible) } + object EvictionWarningOptions { - def apply(): EvictionWarningOptions = - new EvictionWarningOptions(Vector("compile"), Level.Warn) + def default: EvictionWarningOptions = + new EvictionWarningOptions(Vector(Compile), true, true, false, false, defaultGuess) + def full: EvictionWarningOptions = + new EvictionWarningOptions(Vector(Compile), true, true, true, true, defaultGuess) + + lazy val defaultGuess: Function1[(ModuleID, Option[ModuleID], Option[IvyScala]), Boolean] = + guessSecondSegment orElse guessSemVer orElse guessFalse + lazy val guessSecondSegment: PartialFunction[(ModuleID, Option[ModuleID], Option[IvyScala]), Boolean] = { + case (m1, Some(m2), Some(ivyScala)) if m2.name.endsWith("_" + ivyScala.scalaFullVersion) || m2.name.endsWith("_" + ivyScala.scalaBinaryVersion) => + (m1.revision, m2.revision) match { + case (VersionNumber(ns1, ts1, es1), VersionNumber(ns2, ts2, es2)) => + VersionNumber.SecondSegment.isCompatible(VersionNumber(ns1, ts1, es1), VersionNumber(ns2, ts2, es2)) + case _ => false + } + } + lazy val guessSemVer: PartialFunction[(ModuleID, Option[ModuleID], Option[IvyScala]), Boolean] = { + case (m1, Some(m2), _) => + (m1.revision, m2.revision) match { + case (VersionNumber(ns1, ts1, es1), VersionNumber(ns2, ts2, es2)) => + VersionNumber.SemVer.isCompatible(VersionNumber(ns1, ts1, es1), VersionNumber(ns2, ts2, es2)) + case _ => false + } + } + lazy val guessFalse: PartialFunction[(ModuleID, Option[ModuleID], Option[IvyScala]), Boolean] = { + case (_, _, _) => false + } +} + +final class EvictionPair private[sbt] ( + val organization: String, + val name: String, + val winner: Option[ModuleReport], + val evicteds: Seq[ModuleReport], + val includesDirect: Boolean, + val showCallers: Boolean) { + override def toString: String = + EvictionPair.evictionPairLines.showLines(this).mkString +} + +object EvictionPair { + implicit val evictionPairLines: ShowLines[EvictionPair] = ShowLines { a: EvictionPair => + val revs = a.evicteds map { _.module.revision } + val revsStr = if (revs.size <= 1) revs.mkString else "(" + revs.mkString(", ") + ")" + val winnerRev = (a.winner map { r => + val callers: String = + if (a.showCallers) + r.callers match { + case Seq() => "" + case cs => (cs map { _.caller.toString }).mkString(" (caller: ", ", ", ")") + } + else "" + r.module.revision + callers + }).headOption map { " -> " + _ } getOrElse "" + Seq(s"\t* ${a.organization}:${a.name}:${revsStr}$winnerRev") + } +} + +final class EvictionWarning private[sbt] ( + val options: EvictionWarningOptions, + val scalaEvictions: Seq[EvictionPair], + val directEvictions: Seq[EvictionPair], + val transitiveEvictions: Seq[EvictionPair], + val allEvictions: Seq[EvictionPair]) { + def reportedEvictions: Seq[EvictionPair] = scalaEvictions ++ directEvictions ++ transitiveEvictions } object EvictionWarning { - def apply(options: EvictionWarningOptions, report: UpdateReport, log: Logger): Unit = { + def apply(module: IvySbt#Module, options: EvictionWarningOptions, report: UpdateReport, log: Logger): EvictionWarning = { val evictions = buildEvictions(options, report) - processEvictions(evictions, log) + processEvictions(module, options, evictions) } private[sbt] def buildEvictions(options: EvictionWarningOptions, report: UpdateReport): Seq[ModuleDetailReport] = { val buffer: mutable.ListBuffer[ModuleDetailReport] = mutable.ListBuffer() - val confs = report.configurations filter { x => options.configurations contains x.configuration } + val confs = report.configurations filter { x => options.configStrings contains x.configuration } confs flatMap { confReport => confReport.details map { detail => if ((detail.modules exists { _.evicted }) && @@ -31,14 +126,75 @@ object EvictionWarning { buffer.toList.toVector } - private[sbt] def processEvictions(evictions: Seq[ModuleDetailReport], log: Logger): Unit = { - if (!evictions.isEmpty) { - log.warn("Some dependencies were evicted:") - evictions foreach { detail => - val revs = detail.modules filter { _.evicted } map { _.module.revision } - val winner = (detail.modules filterNot { _.evicted } map { _.module.revision }).headOption map { " -> " + _ } getOrElse "" - log.warn(s"\t* ${detail.organization}:${detail.name} (${revs.mkString(", ")})$winner") - } + private[sbt] def isScalaArtifact(module: IvySbt#Module, organization: String, name: String): Boolean = + module.moduleSettings.ivyScala match { + case Some(s) => + organization == s.scalaOrganization && + (name == "scala-library") || (name == "scala-compiler") + case _ => false } + + private[sbt] def processEvictions(module: IvySbt#Module, options: EvictionWarningOptions, reports: Seq[ModuleDetailReport]): EvictionWarning = { + val directDependencies = module.moduleSettings match { + case x: InlineConfiguration => x.dependencies + case _ => Seq() + } + val pairs = reports map { detail => + val evicteds = detail.modules filter { _.evicted } + val winner = (detail.modules filterNot { _.evicted }).headOption + val includesDirect: Boolean = + options.warnDirectEvictions && + (directDependencies exists { dep => + (detail.organization == dep.organization) && (detail.name == dep.name) + }) + new EvictionPair(detail.organization, detail.name, winner, evicteds, includesDirect, options.showCallers) + } + val scalaEvictions: mutable.ListBuffer[EvictionPair] = mutable.ListBuffer() + val directEvictions: mutable.ListBuffer[EvictionPair] = mutable.ListBuffer() + val transitiveEvictions: mutable.ListBuffer[EvictionPair] = mutable.ListBuffer() + def guessCompatible(p: EvictionPair): Boolean = + p.evicteds forall { r => + options.guessCompatible(r.module, p.winner map { _.module }, module.moduleSettings.ivyScala) + } + pairs foreach { + case p if isScalaArtifact(module, p.organization, p.name) => + (module.moduleSettings.ivyScala, p.winner) match { + case (Some(s), Some(winner)) if ((s.scalaFullVersion != winner.module.revision) && options.warnScalaVersionEviction) => + scalaEvictions += p + case _ => + } + case p if p.includesDirect => + if (!guessCompatible(p) && options.warnDirectEvictions) { + directEvictions += p + } + case p => + if (!guessCompatible(p) && options.warnTransitiveEvictions) { + transitiveEvictions += p + } + } + new EvictionWarning(options, scalaEvictions.toList, + directEvictions.toList, transitiveEvictions.toList, pairs) + } + + implicit val evictionWarningLines: ShowLines[EvictionWarning] = ShowLines { a: EvictionWarning => + import ShowLines._ + val out: mutable.ListBuffer[String] = mutable.ListBuffer() + if (!a.scalaEvictions.isEmpty) { + out += "Scala version was updated by one of library dependencies:" + out ++= (a.scalaEvictions flatMap { _.lines }) + } + + if (!a.directEvictions.isEmpty || !a.transitiveEvictions.isEmpty) { + out += "There may be incompatibilities among your library dependencies." + out += "Here are some of the libraries that were evicted:" + out ++= (a.directEvictions flatMap { _.lines }) + out ++= (a.transitiveEvictions flatMap { _.lines }) + } + + if (!a.allEvictions.isEmpty && !a.reportedEvictions.isEmpty && !a.options.showCallers) { + out += "Run 'evicted' to see detailed eviction warnings" + } + + out.toList } } diff --git a/ivy/src/test/scala/BaseIvySpecification.scala b/ivy/src/test/scala/BaseIvySpecification.scala new file mode 100644 index 000000000..e754903fe --- /dev/null +++ b/ivy/src/test/scala/BaseIvySpecification.scala @@ -0,0 +1,51 @@ +package sbt + +import Path._, Configurations._ +import java.io.File +import org.specs2._ +import cross.CrossVersionUtil + +trait BaseIvySpecification extends Specification { + def currentBase: File = new File(".") + def currentTarget: File = currentBase / "target" / "ivyhome" + def defaultModuleId: ModuleID = ModuleID("com.example", "foo", "0.1.0", Some("compile")) + lazy val ivySbt = new IvySbt(mkIvyConfiguration) + lazy val log = Logger.Null + def module(moduleId: ModuleID, deps: Seq[ModuleID], scalaFullVersion: Option[String]): IvySbt#Module = { + val ivyScala = scalaFullVersion map { fv => + new IvyScala( + scalaFullVersion = fv, + scalaBinaryVersion = CrossVersionUtil.binaryScalaVersion(fv), + configurations = Nil, + checkExplicit = true, + filterImplicit = false, + overrideScalaVersion = false) + } + + val moduleSetting: ModuleSettings = InlineConfiguration( + module = moduleId, + moduleInfo = ModuleInfo("foo"), + dependencies = deps, + configurations = Seq(Compile, Test, Runtime), + ivyScala = ivyScala) + new ivySbt.Module(moduleSetting) + } + + def mkIvyConfiguration: IvyConfiguration = { + val paths = new IvyPaths(currentBase, Some(currentTarget)) + val rs = Seq(DefaultMavenRepository) + val other = Nil + val moduleConfs = Seq(ModuleConfiguration("*", DefaultMavenRepository)) + val off = false + val check = Nil + val resCacheDir = currentTarget / "resolution-cache" + val uo = UpdateOptions() + new InlineIvyConfiguration(paths, rs, other, moduleConfs, off, None, check, Some(resCacheDir), uo, log) + } + + def ivyUpdate(module: IvySbt#Module) = { + // IO.delete(currentTarget) + val config = new UpdateConfiguration(None, false, UpdateLogging.Full) + IvyActions.update(module, config, log) + } +} diff --git a/ivy/src/test/scala/EvictionWarningSpec.scala b/ivy/src/test/scala/EvictionWarningSpec.scala new file mode 100644 index 000000000..171d60bd5 --- /dev/null +++ b/ivy/src/test/scala/EvictionWarningSpec.scala @@ -0,0 +1,224 @@ +package sbt + +import org.specs2._ + +class EvictionWarningSpec extends BaseIvySpecification { + def is = s2""" + + This is a specification to check the eviction warnings + + Eviction of scala-library whose scalaVersion should + be detected $scalaVersionWarn1 + not be detected if it's diabled $scalaVersionWarn2 + print out message about the eviction $scalaVersionWarn3 + print out message about the eviction with callers $scalaVersionWarn4 + + Including two (suspect) binary incompatible Java libraries to + direct dependencies should + be detected as eviction $javaLibWarn1 + not be detected if it's disabled $javaLibWarn2 + print out message about the eviction $javaLibWarn3 + print out message about the eviction with callers $javaLibWarn4 + + Including two (suspect) binary compatible Java libraries to + direct dependencies should + not be detected as eviction $javaLibNoWarn1 + print out message about the eviction $javaLibNoWarn2 + + Including two (suspect) transitively binary incompatible Java libraries to + direct dependencies should + be not detected as eviction $javaLibTransitiveWarn1 + be detected if it's enabled $javaLibTransitiveWarn2 + print out message about the eviction if it's enabled $javaLibTransitiveWarn3 + + Including two (suspect) binary incompatible Scala libraries to + direct dependencies should + be detected as eviction $scalaLibWarn1 + print out message about the eviction $scalaLibWarn2 + + Including two (suspect) binary compatible Scala libraries to + direct dependencies should + not be detected as eviction $scalaLibNoWarn1 + print out message about the eviction $scalaLibNoWarn2 + + Including two (suspect) transitively binary incompatible Scala libraries to + direct dependencies should + be not detected as eviction $scalaLibTransitiveWarn1 + be detected if it's enabled $scalaLibTransitiveWarn2 + print out message about the eviction if it's enabled $scalaLibTransitiveWarn3 + """ + + def akkaActor214 = ModuleID("com.typesafe.akka", "akka-actor", "2.1.4", Some("compile")) cross CrossVersion.binary + def akkaActor230 = ModuleID("com.typesafe.akka", "akka-actor", "2.3.0", Some("compile")) cross CrossVersion.binary + def akkaActor234 = ModuleID("com.typesafe.akka", "akka-actor", "2.3.4", Some("compile")) cross CrossVersion.binary + def scala2102 = ModuleID("org.scala-lang", "scala-library", "2.10.2", Some("compile")) + def scala2103 = ModuleID("org.scala-lang", "scala-library", "2.10.3", Some("compile")) + def scala2104 = ModuleID("org.scala-lang", "scala-library", "2.10.4", Some("compile")) + def commonsIo13 = ModuleID("commons-io", "commons-io", "1.3", Some("compile")) + def commonsIo14 = ModuleID("commons-io", "commons-io", "1.4", Some("compile")) + def commonsIo24 = ModuleID("commons-io", "commons-io", "2.4", Some("compile")) + def bnfparser10 = ModuleID("ca.gobits.bnf", "bnfparser", "1.0", Some("compile")) // uses commons-io 2.4 + def unfilteredUploads080 = ModuleID("net.databinder", "unfiltered-uploads", "0.8.0", Some("compile")) cross CrossVersion.binary // uses commons-io 1.4 + def bananaSesame04 = ModuleID("org.w3", "banana-sesame", "0.4", Some("compile")) cross CrossVersion.binary // uses akka-actor 2.1.4 + def akkaRemote234 = ModuleID("com.typesafe.akka", "akka-remote", "2.3.4", Some("compile")) cross CrossVersion.binary // uses akka-actor 2.3.4 + + def defaultOptions = EvictionWarningOptions.default + + import ShowLines._ + + def scalaVersionDeps = Seq(scala2102, akkaActor230) + + def scalaVersionWarn1 = { + val m = module(defaultModuleId, scalaVersionDeps, Some("2.10.2")) + val report = ivyUpdate(m) + EvictionWarning(m, defaultOptions, report, log).scalaEvictions must have size (1) + } + + def scalaVersionWarn2 = { + val m = module(defaultModuleId, scalaVersionDeps, Some("2.10.2")) + val report = ivyUpdate(m) + EvictionWarning(m, defaultOptions.withWarnScalaVersionEviction(false), report, log).scalaEvictions must have size (0) + } + + def scalaVersionWarn3 = { + val m = module(defaultModuleId, scalaVersionDeps, Some("2.10.2")) + val report = ivyUpdate(m) + EvictionWarning(m, defaultOptions, report, log).lines must_== + List("Scala version was updated by one of library dependencies:", + "\t* org.scala-lang:scala-library:2.10.2 -> 2.10.3", + "Run 'evicted' to see detailed eviction warnings") + } + + def scalaVersionWarn4 = { + val m = module(defaultModuleId, scalaVersionDeps, Some("2.10.2")) + val report = ivyUpdate(m) + EvictionWarning(m, defaultOptions.withShowCallers(true), report, log).lines must_== + List("Scala version was updated by one of library dependencies:", + "\t* org.scala-lang:scala-library:2.10.2 -> 2.10.3 (caller: com.typesafe.akka:akka-actor_2.10:2.3.0, com.example:foo:0.1.0)") + } + + def javaLibDirectDeps = Seq(commonsIo14, commonsIo24) + + def javaLibWarn1 = { + val m = module(defaultModuleId, javaLibDirectDeps, Some("2.10.3")) + val report = ivyUpdate(m) + EvictionWarning(m, defaultOptions, report, log).reportedEvictions must have size (1) + } + + def javaLibWarn2 = { + val m = module(defaultModuleId, javaLibDirectDeps, Some("2.10.3")) + val report = ivyUpdate(m) + EvictionWarning(m, defaultOptions.withWarnDirectEvictions(false), report, log).reportedEvictions must have size (0) + } + + def javaLibWarn3 = { + val m = module(defaultModuleId, javaLibDirectDeps, Some("2.10.3")) + val report = ivyUpdate(m) + EvictionWarning(m, defaultOptions, report, log).lines must_== + List("There may be incompatibilities among your library dependencies.", + "Here are some of the libraries that were evicted:", + "\t* commons-io:commons-io:1.4 -> 2.4", + "Run 'evicted' to see detailed eviction warnings") + } + + def javaLibWarn4 = { + val m = module(defaultModuleId, javaLibDirectDeps, Some("2.10.3")) + val report = ivyUpdate(m) + EvictionWarning(m, defaultOptions.withShowCallers(true), report, log).lines must_== + List("There may be incompatibilities among your library dependencies.", + "Here are some of the libraries that were evicted:", + "\t* commons-io:commons-io:1.4 -> 2.4 (caller: com.example:foo:0.1.0)") + } + + def javaLibNoWarn1 = { + val deps = Seq(commonsIo14, commonsIo13) + val m = module(defaultModuleId, deps, Some("2.10.3")) + val report = ivyUpdate(m) + EvictionWarning(m, defaultOptions, report, log).reportedEvictions must have size (0) + } + + def javaLibNoWarn2 = { + val deps = Seq(commonsIo14, commonsIo13) + val m = module(defaultModuleId, deps, Some("2.10.3")) + val report = ivyUpdate(m) + EvictionWarning(m, defaultOptions, report, log).lines must_== Nil + } + + def javaLibTransitiveDeps = Seq(unfilteredUploads080, bnfparser10) + + def javaLibTransitiveWarn1 = { + val m = module(defaultModuleId, javaLibTransitiveDeps, Some("2.10.3")) + val report = ivyUpdate(m) + EvictionWarning(m, defaultOptions, report, log).reportedEvictions must have size (0) + } + + def javaLibTransitiveWarn2 = { + val m = module(defaultModuleId, javaLibTransitiveDeps, Some("2.10.3")) + val report = ivyUpdate(m) + EvictionWarning(m, defaultOptions.withWarnTransitiveEvictions(true), report, log).reportedEvictions must have size (1) + } + + def javaLibTransitiveWarn3 = { + val m = module(defaultModuleId, javaLibTransitiveDeps, Some("2.10.3")) + val report = ivyUpdate(m) + EvictionWarning(m, defaultOptions.withWarnTransitiveEvictions(true).withShowCallers(true), report, log).lines must_== + List("There may be incompatibilities among your library dependencies.", + "Here are some of the libraries that were evicted:", + "\t* commons-io:commons-io:1.4 -> 2.4 (caller: ca.gobits.bnf:bnfparser:1.0, net.databinder:unfiltered-uploads_2.10:0.8.0)") + } + + def scalaLibWarn1 = { + val deps = Seq(scala2104, akkaActor214, akkaActor234) + val m = module(defaultModuleId, deps, Some("2.10.4")) + val report = ivyUpdate(m) + EvictionWarning(m, defaultOptions, report, log).reportedEvictions must have size (1) + } + + def scalaLibWarn2 = { + val deps = Seq(scala2104, akkaActor214, akkaActor234) + val m = module(defaultModuleId, deps, Some("2.10.4")) + val report = ivyUpdate(m) + EvictionWarning(m, defaultOptions, report, log).lines must_== + List("There may be incompatibilities among your library dependencies.", + "Here are some of the libraries that were evicted:", + "\t* com.typesafe.akka:akka-actor_2.10:2.1.4 -> 2.3.4", + "Run 'evicted' to see detailed eviction warnings") + } + + def scalaLibNoWarn1 = { + val deps = Seq(scala2104, akkaActor230, akkaActor234) + val m = module(defaultModuleId, deps, Some("2.10.4")) + val report = ivyUpdate(m) + EvictionWarning(m, defaultOptions, report, log).reportedEvictions must have size (0) + } + + def scalaLibNoWarn2 = { + val deps = Seq(scala2104, akkaActor230, akkaActor234) + val m = module(defaultModuleId, deps, Some("2.10.4")) + val report = ivyUpdate(m) + EvictionWarning(m, defaultOptions, report, log).lines must_== Nil + } + + def scalaLibTransitiveDeps = Seq(scala2104, bananaSesame04, akkaRemote234) + + def scalaLibTransitiveWarn1 = { + val m = module(defaultModuleId, scalaLibTransitiveDeps, Some("2.10.4")) + val report = ivyUpdate(m) + EvictionWarning(m, defaultOptions, report, log).reportedEvictions must have size (0) + } + + def scalaLibTransitiveWarn2 = { + val m = module(defaultModuleId, scalaLibTransitiveDeps, Some("2.10.4")) + val report = ivyUpdate(m) + EvictionWarning(m, defaultOptions.withWarnTransitiveEvictions(true), report, log).reportedEvictions must have size (1) + } + + def scalaLibTransitiveWarn3 = { + val m = module(defaultModuleId, scalaLibTransitiveDeps, Some("2.10.4")) + val report = ivyUpdate(m) + EvictionWarning(m, defaultOptions.withWarnTransitiveEvictions(true).withShowCallers(true), report, log).lines must_== + List("There may be incompatibilities among your library dependencies.", + "Here are some of the libraries that were evicted:", + "\t* com.typesafe.akka:akka-actor_2.10:2.1.4 -> 2.3.4 (caller: com.typesafe.akka:akka-remote_2.10:2.3.4, org.w3:banana-sesame_2.10:0.4, org.w3:banana-rdf_2.10:0.4)") + } +}