From 5b1c33dd6e818c08a80822532be8ecb1c5d00d9e Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Sat, 22 Mar 2014 02:47:11 -0400 Subject: [PATCH] Added conflict report and unit tests --- main/src/main/scala/sbt/Plugins.scala | 32 +++++++-- main/src/test/scala/PluginsTest.scala | 96 +++++++++++++++++++++++++++ 2 files changed, 124 insertions(+), 4 deletions(-) create mode 100644 main/src/test/scala/PluginsTest.scala diff --git a/main/src/main/scala/sbt/Plugins.scala b/main/src/main/scala/sbt/Plugins.scala index efe8f28f5..cb7bcac6e 100644 --- a/main/src/main/scala/sbt/Plugins.scala +++ b/main/src/main/scala/sbt/Plugins.scala @@ -64,6 +64,8 @@ abstract class AutoPlugin extends Plugins.Basic with PluginsFunctions val label: String = getClass.getName.stripSuffix("$") + override def toString: String = label + /** The [[Configuration]]s to add to each project that activates this AutoPlugin.*/ def projectConfigurations: Seq[Configuration] = Nil @@ -169,8 +171,7 @@ object Plugins extends PluginsFunctions val forbidden: Set[AutoPlugin] = (selectedPlugins flatMap { Plugins.asExclusions }).toSet val c = selectedPlugins.toSet & forbidden if (!c.isEmpty) { - val listString = (c map {_.label}).mkString(", ") - throw AutoPluginException(s"Contradiction in selected plugins. These plguins were both included and excluded: ${listString}") + exlusionConflictError(requestedPlugins, selectedPlugins, c.toSeq sortBy {_.label}) } val retval = topologicalSort(selectedPlugins, log) log.debug(s" :: sorted deduced result: ${retval.toString}") @@ -193,7 +194,7 @@ object Plugins extends PluginsFunctions doSort(roots, nonRoots, ns.size * ns.size + 1) } private[sbt] def translateMessage(e: LogicException) = e match { - case ic: InitialContradictions => s"Contradiction in selected plugins. These plguins were both included and excluded: ${literalsString(ic.literals.toSeq)}" + case ic: InitialContradictions => s"Contradiction in selected plugins. These plugins were both included and excluded: ${literalsString(ic.literals.toSeq)}" case io: InitialOverlap => s"Cannot directly enable plugins. Plugins are enabled when their required plugins are satisifed. The directly selected plugins were: ${literalsString(io.literals.toSeq)}" case cn: CyclicNegation => s"Cycles in plugin requirements cannot involve excludes. The problematic cycle is: ${literalsString(cn.cycle)}" } @@ -208,6 +209,29 @@ object Plugins extends PluginsFunctions val message = s"Plugin$ns provided by multiple AutoPlugins:$nl${dupStrings.mkString(nl)}" throw AutoPluginException(message) } + private[this] def exlusionConflictError(requested: Plugins, selected: Seq[AutoPlugin], conflicting: Seq[AutoPlugin]) { + def listConflicts(ns: Seq[AutoPlugin]) = (ns map { c => + val reasons = (if (flatten(requested) contains c) List("requested") + else Nil) ++ + (if (c.requires != empty && c.trigger == allRequirements) List(s"enabled by ${c.requires.toString}") + else Nil) ++ + { + val reqs = selected filter { x => asRequirements(x) contains c } + if (!reqs.isEmpty) List(s"""required by ${reqs.mkString(", ")}""") + else Nil + } ++ + { + val exs = selected filter { x => asExclusions(x) contains c } + if (!exs.isEmpty) List(s"""excluded by ${exs.mkString(", ")}""") + else Nil + } + s""" - conflict: ${c.label} is ${reasons.mkString("; ")}""" + }).mkString("\n") + throw AutoPluginException(s"""Contradiction in enabled plugins: + - requested: ${requested.toString} + - enabled: ${selected.mkString(", ")} +${listConflicts(conflicting)}""") + } private[sbt] final object Empty extends Plugins { def &&(o: Basic): Plugins = o @@ -225,7 +249,7 @@ object Plugins extends PluginsFunctions } private[sbt] final case class And(plugins: List[Basic]) extends Plugins { def &&(o: Basic): Plugins = And(o :: plugins) - override def toString = plugins.mkString(", ") + override def toString = plugins.mkString(" && ") } private[sbt] def and(a: Plugins, b: Plugins) = b match { case Empty => a diff --git a/main/src/test/scala/PluginsTest.scala b/main/src/test/scala/PluginsTest.scala new file mode 100644 index 000000000..a558854f1 --- /dev/null +++ b/main/src/test/scala/PluginsTest.scala @@ -0,0 +1,96 @@ +package sbt + +import java.io.File +import org.specs2._ +import mutable.Specification + +object PluginsTest extends Specification +{ + import AI._ + + "Auto plugin" should { + "enable plugins with trigger=allRequirements AND requirements met" in { + deducePlugin(A && B, log) must contain(Q) + } + "enable transive plugins with trigger=allRequirements AND requirements met" in { + deducePlugin(A && B, log) must contain(R) + } + "order enable plugins after required plugins" in { + val ns = deducePlugin(A && B, log) + ( (ns indexOf Q) must beGreaterThan(ns indexOf A) ) and + ( (ns indexOf Q) must beGreaterThan(ns indexOf B) ) and + ( (ns indexOf R) must beGreaterThan(ns indexOf A) ) and + ( (ns indexOf R) must beGreaterThan(ns indexOf B) ) and + ( (ns indexOf R) must beGreaterThan(ns indexOf Q) ) + } + "not enable plugins with trigger=allRequirements but conflicting requirements" in { + deducePlugin(A && B, log) must not contain(S) + } + "enable plugins that are required by the requested plugins" in { + val ns = deducePlugin(Q, log) + (ns must contain(A)) and + (ns must contain(B)) + } + "throw an AutoPluginException on conflicting requirements" in { + deducePlugin(S, log) must throwAn[AutoPluginException](message = """Contradiction in enabled plugins: + - requested: sbt.AI\$S + - enabled: sbt.AI\$S, sbt.AI\$Q, sbt.AI\$R, sbt.AI\$B, sbt.AI\$A + - conflict: sbt.AI\$R is enabled by sbt.AI\$Q; excluded by sbt.AI\$S""") + } + "generates a detailed report on conflicting requirements" in { + deducePlugin(T && U, log) must throwAn[AutoPluginException](message = """Contradiction in enabled plugins: + - requested: sbt.AI\$T && sbt.AI\$U + - enabled: sbt.AI\$U, sbt.AI\$T, sbt.AI\$A, sbt.AI\$Q, sbt.AI\$R, sbt.AI\$B + - conflict: sbt.AI\$Q is enabled by sbt.AI\$A && sbt.AI\$B; required by sbt.AI\$T, sbt.AI\$R; excluded by sbt.AI\$U + - conflict: sbt.AI\$R is enabled by sbt.AI\$Q; excluded by sbt.AI\$T""") + } + } +} + +object AI +{ + lazy val allPlugins: List[AutoPlugin] = List(A, B, Q, R, S, T, U) + lazy val deducePlugin = Plugins.deducer(allPlugins) + lazy val log = Logger.Null + + trait EmptyAutoPlugin extends AutoPlugin { + def requires = empty + def trigger = noTrigger + } + object A extends EmptyAutoPlugin + object B extends EmptyAutoPlugin + + object Q extends AutoPlugin + { + def requires: Plugins = A && B + def trigger = allRequirements + } + + object R extends AutoPlugin + { + def requires = Q + def trigger = allRequirements + } + + object S extends AutoPlugin + { + def requires = Q && !R + def trigger = allRequirements + } + + // This is an opt-in plugin with a requirement + // Unless explicitly loaded by the build user, this will not be activated. + object T extends AutoPlugin + { + def requires = Q && !R + def trigger = noTrigger + } + + // This is an opt-in plugin with a requirement + // Unless explicitly loaded by the build user, this will not be activated. + object U extends AutoPlugin + { + def requires = A && !Q + def trigger = noTrigger + } +}